一、基本概念
乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题。
- 乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。
- 悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。
二、实现方式(含实例)
在说明实现方式之前,需要明确:乐观锁和悲观锁是两种思想,它们的使用是非常广泛的,不局限于某种编程语言或数据库。
悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如Java的synchronized关键字),也可以是对数据加锁(如MySQL中的排它锁)。
乐观锁的实现方式主要有两种:CAS机制和版本号机制,下面详细介绍。
2.1 CAS(Compare And Swap)
CAS操作包括了3个操作数:
- 需要读写的内存位置(V)
- 进行比较的预期值(A)
- 拟写入的新值(B)
CAS操作逻辑如下:如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。许多CAS的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。
这里引出一个新的问题,既然CAS包含了Compare和Swap两个操作,它又如何保证原子性呢?
答案是:CAS是由CPU支持的原子操作,其原子性是在硬件层面进行保证的。
下面以Java中的自增操作(i++)为例,看一下悲观锁和CAS分别是如何保证线程安全的。我们知道,在Java中自增操作不是原子操作,它实际上包含三个独立的操作:
(1)读取i值;(2)加1;(3)将新值写回i
因此,如果并发执行自增操作,可能导致计算结果的不准确。在下面的代码示例中:value1没有进行任何线程安全方面的保护,value2使用了乐观锁(CAS),value3使用了悲观锁(synchronized)。运行程序,使用1000个线程同时对value1、value2和value3进行自增操作,可以发现:value2和value3的值总是等于1000,而value1的值常常小于1000。
public class Test {
private static int value1 = 0; //线程不安全
private static AtomicInteger value2 = new AtomicInteger(0); //乐观锁
private static int value3 = 0; //悲观锁
private static synchronized void increateValue3(){
value3++;
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
value1++;
value2.getAndIncrement();
increateValue3();
}
}).start();
}
Thread.sleep(1000);
System.out.println("线程不安全:" + value1);
System.out.println("乐观锁(AtomicInteger):" + value2);
System.out.println("悲观锁(synchronized):" + value3);
}
}
首先来介绍AtomicInteger。AtomicInteger是java.util.concurrent.atomic包提供的原子类,利用CPU提供的CAS操作来保证原子性;除了AtomicInteger外,还有AtomicBoolean、AtomicLong、AtomicReference等众多原子类。
下面看一下AtomicInteger的源码,了解下它的自增操作getAndIncrement()是如何实现的(源码以Java8为例)。
package java.util.concurrent.atomic;
import sun.misc.Unsafe;
import java.util.function.IntBinaryOperator;
import java.util.function.IntUnaryOperator;
/**
* 原子类
*/
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// Unsafe可以执行低级别、不安全操作的方法,比如直接访问系统内存资源、自主管理内存资源等
// 使用Unsafe,可以实现内存操作、CAS、内存屏障...在AtomicInteger中,主要用来进行CAS操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
// value就是AtomicInteger保存的值,因为加了volatile关键字,所以在并发操作时,多线程能感知数据发生变化
private volatile int value;
// 该字段用来保存内存偏移地址,会在下面的静态代码块初始化
private static final long valueOffset;
static {
try {
// valueOffset为字段value的内存偏移地址(相对于atomicInteger对象基地址的偏移)
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
}
/**
* 创建AtomicInteger对象,并设置初始值
*
* @param initialValue 初始值
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
/**
* 创建AtomicInteger对象,初始值为0
*/
public AtomicInteger() {
}
/**
* 获取value.
*
* @return 当前的value
*/
public final int get() {
return value;
}
/**
* 立即修改或者设置value
* set操作能够保证可见性,避免指令重排
*
* @param newValue 设置的新值
*/
public final void set(int newValue) {
value = newValue;
}
/**
* 不会立即修改或者设置值(但是最终会)
* lazySet不能保证可见性,可能会发生指令重排,但是性能比set高
*
* @param newValue 要设置的值
* @since 1.6
*/
public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}
/**
* 设置新值,并且返回旧值(原子操作)
*
* @param newValue 新值
* @return 旧值
*/
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
/**
* 原子操作,CAS,当value和expect相等时,才将value修改为update
*
* @param expect 期望value的值
* @param update 要修改的值
* @return true:value和expect相等,且完成修改;false:value和expect不相等
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
/**
* 和compareAndSet相同功能,都是使用CAS原子操作,但是无法保证多个线程CAS的有序性
*
* @param expect 期望的value值
* @param update 更改后的值
* @return 操作是否成功
*/
public final boolean weakCompareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
/**
* 将value加1,然后返回旧值,原子操作
*
* @return 旧值
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
/**
* 将value减1,然后返回旧值,原子操作
*
* @return 旧值
*/
public final int getAndDecrement() {
return unsafe.getAndAddInt(this, valueOffset, -1);
}
/**
* 对value增加指定值,然后返回旧值,原子操作
*
* @param delta 要加的值
* @return 旧值
*/
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
/**
* 将value加1,并返回新值,原子操作
*
* @return value加1后的值
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
/**
* 将value减1,并返回新值,原子操作
*
* @return value减1后的新值
*/
public final int decrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}
/**
* 对value增加值,然后返回新值,原子操作
*
* @param delta 新增的值
* @return value新增后的值
*/
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
/**
* 传入Function,对value进行操作(同样使用CAS保证原子性),会一直重试直到成功才中断,然后返回旧值
*
* @param updateFunction a side-effect-free function
* @return 旧值
* @since 1.8
*/
public final int getAndUpdate(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return prev;
}
/**
* 传入Function,对value进行操作(同样使用CAS保证原子性),会一直重试直到成功才中断,然后返回新值
*
* @param updateFunction a side-effect-free function
* @return the updated value
* @since 1.8
*/
public final int updateAndGet(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
// 一直重试,直到CAS操作成功
} while (!compareAndSet(prev, next));
return next;
}
/**
* Atomically updates the current value with the results of
* applying the given function to the current and given values,
* returning the previous value. The function should be
* side-effect-free, since it may be re-applied when attempted
* updates fail due to contention among threads. The function
* is applied with the current value as its first argument,
* and the given update as the second argument.
*
* @param x the update value
* @param accumulatorFunction a side-effect-free function of two arguments
* @return the previous value
* @since 1.8
*/
public final int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction) {
int prev, next;
do {
prev = get();
next = accumulatorFunction.applyAsInt(prev, x);
} while (!compareAndSet(prev, next));
return prev;
}
/**
* Atomically updates the current value with the results of
* applying the given function to the current and given values,
* returning the updated value. The function should be
* side-effect-free, since it may be re-applied when attempted
* updates fail due to contention among threads. The function
* is applied with the current value as its first argument,
* and the given update as the second argument.
*
* @param x the update value
* @param accumulatorFunction a side-effect-free function of two arguments
* @return the updated value
* @since 1.8
*/
public final int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction) {
int prev, next;
do {
prev = get();
next = accumulatorFunction.applyAsInt(prev, x);
} while (!compareAndSet(prev, next));
return next;
}
public String toString() {
return Integer.toString(get());
}
public int intValue() {
return get();
}
public long longValue() {
return (long) get();
}
public float floatValue() {
return (float) get();
}
public double doubleValue() {
return (double) get();
}
}
源码分析说明如下:
- getAndIncrement()实现的自增操作是自旋CAS操作:在循环中进行compareAndSet,如果执行成功则退出,否则一直执行。
- 其中compareAndSet是CAS操作的核心,它是利用Unsafe对象实现的。
- Unsafe又是何许人也呢?Unsafe是用来帮助Java访问操作系统底层资源的类(如可以分配内存、释放内存),通过Unsafe,Java具有了底层操作能力,可以提升运行效率;强大的底层资源操作能力也带来了安全隐患(类的名字Unsafe也在提醒我们这一点),因此正常情况下用户无法使用。AtomicInteger在这里使用了Unsafe提供的CAS功能。
- valueOffset可以理解为value在内存中的偏移量,对应了CAS三个操作数(V/A/B)中的V;偏移量的获得也是通过Unsafe实现的。
- value域的volatile修饰符:Java并发编程要保证线程安全,需要保证原子性、可视性和有序性;CAS操作可以保证原子性,而volatile可以保证可视性和一定程度的有序性;在AtomicInteger中,volatile和CAS一起保证了线程安全性。关于volatile作用原理的说明涉及到Java内存模型(JMM),这里不详细展开。
说完了AtomicInteger,再说synchronized。synchronized通过对代码块加锁来保证线程安全:在同一时刻,只能有一个线程可以执行代码块中的代码。synchronized是一个重量级的操作,不仅是因为加锁需要消耗额外的资源,还因为线程状态的切换会涉及操作系统核心态和用户态的转换;不过随着JVM对锁进行的一系列优化(如自旋锁、轻量级锁、锁粗化等),synchronized的性能表现已经越来越好。
2.2 版本号机制
除了CAS,版本号机制也可以用来实现乐观锁。版本号机制的基本思路是在数据中增加一个字段version,表示该数据的版本号,每当数据被修改,版本号加1。当某个线程查询数据时,将该数据的版本号一起查出来;当该线程更新数据时,判断当前版本号与之前读取的版本号是否一致,如果一致才进行操作。
需要注意的是,这里使用了版本号作为判断数据变化的标记,实际上可以根据实际情况选用其他能够标记数据版本的字段,如时间戳等。
为了更好地理解乐观锁/悲观锁的具体实现,我们可以使用一个简单的银行账户余额更新作为例子。我们假设有一个名为 accounts
的表,其中包括字段 account_id
(账户标识)、balance
(账户余额)和 version
(版本号,用于乐观锁)。
乐观锁实现
SQL 表结构
CREATE TABLE accounts (
account_id INT AUTO_INCREMENT PRIMARY KEY,
balance DECIMAL(10, 2),
version INT DEFAULT 1 );
更新操作
在进行更新操作时,检查 version 字段确保数据未被修改过,然后进行更新,并将 version 字段的值增加1。
UPDATE accounts
SET balance = balance + 1000, -- 假设我们要增加1000元
version = version + 1
WHERE account_id = 1 AND version = @CurrentVersion;
@CurrentVersion 是从应用程序传入的,基于最初查询得到的版本号
如果 version 不匹配,即另一个事务已经更新了记录,这个更新操作将不会改变任何行,应用程序可以据此知道更新失败,可能需要重新尝试或通知用户。
悲观锁实现
在使用悲观锁时,可以利用数据库提供的锁机制(如行锁),确保在当前事务完成之前,其他事务不能修改被锁定的数据。
SQL 表结构
-- 使用与乐观锁相同的表结构
CREATE TABLE accounts (
account_id INT AUTO_INCREMENT PRIMARY KEY,
balance DECIMAL(10, 2)
);
更新操作
在查询时使用 FOR UPDATE
语句来锁定数据行。这会在当前事务完成之前阻止其他事务修改这些行。
START TRANSACTION;
SELECT balance
FROM accounts
WHERE account_id = 66
FOR UPDATE; -- 锁定账户66
-- 执行业务逻辑,比如增加余额1000
UPDATE accounts
SET balance = balance + 1000
WHERE account_id = 1;
COMMIT;
在这个例子中,SELECT ... FOR UPDATE
语句将阻止其他任何尝试更新或读取(在需要相同锁的情况下)account_id
为 66 的行的事务,直到当前事务提交或回滚。
三、优缺点和适用场景
乐观锁和悲观锁并没有优劣之分,它们有各自适合的场景;下面从两个方面进行说明。
3.1 功能限制
与悲观锁相比,乐观锁适用的场景受到了更多的限制,无论是CAS还是版本号机制。
例如,CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而synchronized则可以通过对整个代码块加锁来处理。再比如版本号机制,如果query的时候是针对表1,而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁。
3.2 竞争激烈程度
如果悲观锁和乐观锁都可以使用,那么选择就要考虑竞争的激烈程度:
- 当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。
- 当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。
乐观锁和悲观锁各有用武之地,具体使用哪种锁机制取决于应用场景和并发级别。乐观锁适用于写操作较少的场景,悲观锁则适用于高冲突环境,尤其是在多用户频繁写入同一数据的场合。
四、知识点补充
4.1 乐观锁加锁吗?
- 乐观锁本身是不加锁的,只是在更新时判断一下数据是否被其他线程更新了,AtomicInteger便是一个例子。
4.2 CAS有哪些缺点?
ABA问题
假设有两个线程——线程1和线程2,两个线程按照顺序进行以下操作:
- 线程1读取内存中数据为A;
- 线程2将该数据修改为B;
- 线程2将该数据修改为A;
- 线程1对数据进行CAS操作
在第(4)步中,由于内存中数据仍然为A,因此CAS操作成功,但实际上该数据已经被线程2修改过了,这就是ABA问题。在某些场景下,ABA会带来隐患,例如栈顶问题:一个栈的栈顶经过两次(或多次)变化又恢复了原值,但是栈可能已发生了变化。
高竞争下的开销问题
在并发冲突概率大的高竞争环境下,如果CAS一直失败,会一直重试,CPU开销较大。针对这个问题的一个思路是引入退出机制,如重试次数超过一定阈值后失败退出。当然,更重要的是避免在高竞争环境下使用乐观锁。
功能限制
CAS的功能是比较受限的,例如CAS只能保证单个变量(或者说单个内存值)操作的原子性,这意味着:
(1)原子性不一定能保证线程安全,例如在Java中需要与volatile配合来保证线程安全;
(2)当涉及到多个变量(内存值)时,CAS也无能为力。
除此之外,CAS的实现需要硬件层面处理器的支持,在Java中普通用户无法直接使用,只能借助atomic包下的原子类使用,灵活性受到限制。