Java-併發-CAS
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
,只是實現的原理有很小的不同而已。