Java-并发-CAS
0x01 摘要
本文主要讲讲AQS
(AbstractQueuedSynchronizer)中大量使用的CAS
,以及著名的ABA
问题。
0x02 CAS基本概念
乐观锁在Java中的一个重要实现就是CAS
,全称为 Compare and Swap
,就是在内存级别比较和原子性地替换值。
在Java里,是用的sun.misc.Unsafe
类来实现了很多相关的native
修饰的CAS
方法。最常用的应该就是compareAndSwapInt
方法。
可以看看ReentrantLock
里的lock()
方法:
public void lock() {
// 使用同步锁进行锁定
sync.lock();
}
接着看看默认的非公平同步锁的lock
方法的实现:
final void lock() {
// 这一步其实就是期望的值是0,如果确实是0就把它更新为1
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
这里就是调用的AQS里的compareAndSetState
方法
// 用来表示同步锁状态的变量
private volatile int state;
// 上述状态变量在AbstractQueuedSynchronizer类中的域偏移值
private static final long stateOffset;
stateOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
// 在当前state变量值等于excpect时,就原子性的设置state变量为给定的update值
protected final boolean compareAndSetState(int expect, int update) {
// 尝试用CAS的方法将当前AQS实例state变量设为1
// 如果成功就返回true
// 返回false意味着该变量的当前值不为expect
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
0x03 ABA问题
3.1 什么是ABA问题
CAS看起来很方便,好像啥都不用我们操心,但有可能会导致著名的ABA
问题。
我们仔细考虑,CAS操作其实分为两步:
- 获取某时刻该变量在offset的值
- 比较并替换该值
也就是说,1和2不是一个原子性操作,那么就有可能发生获取的时候是期望值,但在比较和替换时其实该值已经被其他线程修改了的情况,导致有一次CAS操作的结果被覆盖。这就是ABA问题。
具体来说,这个ABA过程如下:
- 线程1做CAS(X, A, C),获取到该变量的X值为A
- 线程2做CAS(X, A, B),获取到该变量X的值为A符合预期
- 线程2将X值设为B
- 线程2做CAS(X, B, A),获取到该变量X的值为B符合预期
- 线程2又将X值设为A
- 线程1发现X值为A符合CAS传入的预期值,于是将X的值设为C
- 结果就是线程1和2都认为此次CAS操作成功,但其实里面有个中间变化线程2根本就不知道,这并不符合我们的预期。可能会导致意外的严重后果。
对于使用CAS操作的原子类,例如AtomicInteger,均存在ABA问题,所以我们通常会对变量增加版本号来解决问题,JDK中的AtomicStampedReference也帮我们实现了这个功能,不过这里需要注意存在一个坑。
3.2 ABA问题示例
private static AtomicInteger atomicInt = new AtomicInteger(100);
public static void main(String[] args) throws InterruptedException {
Thread intT1 = new Thread(new Runnable() {
@Override
public void run() {
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 100);
}
});
Thread intT2 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean c3 = atomicInt.compareAndSet(100, 101);
//true
System.out.println(c3);
}
});
intT1.start();
intT2.start();
}
最后会发现线程二输出的cas结果为true,也就是说他没有感知到值变量atomicInt
被被修改后又被重置为100
。
3.3 AtomicStampedReference原理
3.3.1 重要的内部类和构造方法
// AtomicStampedReference的内部类Pair
// 用来存放关心的对象和版本戳之间的关联
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
private volatile Pair<V> pair;
/**
* 使用指定的初值创建一个新的AtomicStampedReference
*
* @param initialRef 我们关心的对象的初值
* @param initialStamp 版本戳的初值
*/
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
3.3.2 compareAndSet
/**
* 使用期望的目标对象和版本戳来设值
* 1.比较当前目标引用和目标对象引用地址
* 2.然后比较当前版本戳和参数中的期望版本戳是否相同
* 3.比较当前目标引用和新的目标对象引用是否相同且当前版本戳和新的目标版本戳相同,说明已经被其他线程修改
* 4.或是成功以CAS方式修改了<当前目标对象, 当前版本戳>为新的<新目标对象, 新版本戳>
* 5. 3和4满足任意条件就返回true
*
* @param expectedReference the expected value of the reference
* @param newReference the new value for the reference
* @param expectedStamp the expected value of the stamp
* @param newStamp the new value for the stamp
* @return {@code true} if successful
*/
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
3.3.3 casPair
// 这个方法就是用UNSAFE类来直接CAS方式替换Pair对象
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
3.3.4 小结
其实AtomicStampedReference
就是在普通CAS基础上加上了个版本戳,和对象形成了Pair,可以避免ABA问题。
3.4 AtomicStampedReference解决ABA问题示例
private static AtomicStampedReference<Integer> atomicStampedRef =
new AtomicStampedReference<Integer>(100, 0);
public static void main(String[] args) throws InterruptedException {
Thread refT1 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedRef.compareAndSet(100, 101,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
atomicStampedRef.compareAndSet(101, 100,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
}
});
Thread refT2 = new Thread(new Runnable() {
@Override
public void run() {
int stamp = atomicStampedRef.getStamp();
System.out.println("before sleep : stamp = " + stamp); // stamp = 0
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1
boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1);
//false
System.out.println(c3);
}
});
refT1.start();
refT2.start();
}
最终会发现线程2输出的CAS结果为false,因为与atomicStampedRef关联的stamp因为线程1的操作导致已经发生了变化。
0x04 总结
以上就是对CAS和ABA问题的分析,可以看到java中的解决方法就是将对象和另一个对象关联组成pair,然后通过UNSAFE直接CAS替换该pair对象。与上面提到的AtomicStampedReference
类似的还有AtomicMarkableReference
,只是实现的原理有很小的不同而已。