悲观锁和乐观锁的实现并不是Java独有的,但在JUC中却有较多类的实现体现了这种看待线程同步的理念。所以我写篇博客加强记忆一下。
一、基本概念
概念上悲观锁比较容易理解。在对于一个共享数据的并发操作上,悲观锁认为本线程在进行数据操作的过程中一定会有其他的线程来修改数据,所以在获取共享数据的时候会先加锁,阻止共享数据被其他线程修改。像synchronized关键字或者Lock的实现类采用的就是悲观锁的概念:
// ===== synchronized:synchronized的实现其实是一种悲观锁
synchronized void method() {
// handle public resource
}
// ===== ReentrantLock:Lock的实现类
private ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock();
// handle public resource
lock.unlock();
}
而乐观锁与悲观锁最大的不同点就是认为本线程在进行数据操作的时候不会有别的线程对数据进行修改,所以在操作数据的时候不进行上锁。但是我们其实并不能真的保证没有其他线程干涉到我们要操作的数据,所以在线程修改数据之前,我们会将该数据拿出来判断一下有没有其他的线程更新了这个数据。Java中,像java.util.concurrent.atomic
包下的许多类的实现采用了这种概念:
// ===== AtomicInteger底层实现采用了乐观锁概念
private AtomicInteger atomicInteger = new AtomicInteger();
public void method() {
atomicInteger.incrementAndGet();
}
二、CAS实现乐观锁
虽然我们假设乐观锁是不会发生冲突的,但是实际不然,前文也提到我们在最后修改数据时需要将数据拿出来判断。在这里我们可能会想到volatile
修饰符,将其拿出来判断。但是volatile
本身不能保证原子性,也不适用于这种在验证正确性时需要对可见性进行过于复杂判断场景。实现乐观锁的最常用的实现方式就是CAS了。
Compare And Swap,简称CAS。意思其实就是比较与交换,是一种无锁算法,它主要包括了三个操作步骤:
- 比较原数据与期望值是否相等。(比较)
- 如果比较相等,将新数据写入原数据。(交换)
- 返回此次操作是否成功。
在阅读源码的时候,可以发现无论是ReentrantLock
的AQS
的实现,还是java.util.concurrent.atomic
包下的各种以Atomic开头的原子类,其底层都会调用到Unsafe
类的compareAndSwap()
函数。我们以AtomicInteger类的getAndIncrement()
函数举例,它最终调用compareAndSwapInt
这个本地函数。
// ===== AtomicInteger类
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// ===== Unsafe类
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
// ===== Unsafe类的本地方法
public final native boolean compareAndSwapInt(Object, long, int, int);
我们现在对compareAndSwapInt()
函数进行分析,不过在进行分析之前我们需要先了解这个函数传入的var1
、var2
、var5
、var5 + var4
这四个参数代表了什么含义。结合AtomicInteger的源码来看:
public class AtomicBoolean implements java.io.Serializable {
private static final long serialVersionUID = 4654671469794556979L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
// ===== static:静态代码块,加载时初始化
static {
try {
// ===== valueOffset:记录value的偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicBoolean.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// ===== value:数据的存放
private volatile int value;
......
}
从上面代码中我们可以发现,AtomicInteger类定义了value
字段来存储实际的值,并且得到了一个unsafe
实例,在类初始化时,代码就通过unsafe实例的objectFieldOffset()
函数得到了value的偏移量,在这里我们可以看出valueoffset
其实就是用来记录value的偏移量的,而通过代码调用可以看出最终的var2参数其实就是valueoffset
。而var1
从函数的调用关系显然易见是this
。
而从getAndInt
这个函数可以看出var5
是getIntVolatile(var1, var2)
函数的返回值,这个函返回值其实代表的就是var1中,var2偏移量处的返回值,var5代表的是内存中现在valueOffset
的值。而var4
是从实参处传过来的1
,所以var5 + var4
就是var5 + 1
。
我们将this.compareAndSwapInt(var1, var2, var5, var5 + var4)
表示为this.compareAndSwapInt(this, offset, expect, valueOffset)
可能更容易理解些。
根据源码我们可以看出,getAndAddInt()
循环获取给定this对象中的偏移量处的值expect(即var5)
,然后判断内存值是否等于expect
。如果相等则将内存值设置为expect + 1
,否则返回false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。整个“比较+更新”操作封装在compareAndSwapInt()
中,乍一看这也是两个步骤(比较,交换),其实在JNI里是借助于一个CPU(cmpxchg)指令完成的,可以保证多个线程都能够看到同一个变量的修改值,所以还是原子操作。
三、CAS所带来的问题
1. ABA问题
CAS需要检查操作值有没有发生改变,如果没有发生改变则更新。但是如果一个值原来是A,变成了B,然后又变成了A,那么在CAS检查的时候会发现没有改变,但是实质上它已经发生了改变,这就是所谓的ABA问题。对于ABA问题其解决方案是加上版本号,即在每个变量都加上一个版本号,每次改变时加1,即A —> B —> A,变成1A —> 2B —> 3A。
- JDK从1.5开始提供了
AtomicStampedReference
类来解决ABA问题,具体操作封装在compareAndSet()
中。compareAndSet()
首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
2. 系统资源开销大
上面我们提到过,CAS操作不成功,会导致其一直自旋,长期的自旋会给CPU带来非常大的开销。
- 在JUC中有些类的实现就限制了CAS自旋的次数以达到降低系统开销的目的,例如
BlockingQueue
的SynchronousQueue
。
3. 只确保一个共享变量原子性
CAS只能对一个变量在执行操作时保证其原子性,而对与多个变量,CAS则无法保证其操作的原子性。
- JDK从1.5开始提供了
AtomicReference
类来保证引用对象之间的原子性,我们可以通过把多个变量放在一个对象里的方法来进行对多个共享变量的CAS操作。
结语
综上所属,我们可以了解到:
- 悲观锁适合写操作多的场景,对数据进行修改前加锁可以保证操作的正确;
- 乐观锁适合读操作多的场景,不对数据加锁可以使读操作的性能有很大提升。
参考