java並發之CAS詳解
前言
在高並發的應用當中,最關鍵的問題就是對共享變量的安全訪問,通常我們都是通過加鎖的方式,比如說synchronized、Lock來保證原子性,或者在某些應用當中,用voliate來保證變量的可見性,還有就是通過TheadLocal將變量copy一份,稱為局部變量(線程私有)等等。現在我們學習一種不加鎖機制(CAS)
上述我們提到的synchronized和Lock這都是通過加鎖實現的(悲觀鎖),其實加鎖本質上是將並發轉變成串行實現的,勢必會阻塞線程的執行,影響應用的吞吐量,而CAS正是一種樂觀的策略,並不會出現加鎖來阻塞線程的執行。
CAS簡介
CAS,Compare And Swap,即比較並交換。Atomic原子類操作等等都是以CAS實現的,還有ConcurrentHashMap在JDK1.8的版本也調整為了CAS+Synchronized
分析(自旋中對應下文的native方法)
在CAS中有三個參數:內存值V、舊的預期值A、要更新的值B。當且僅當內存值V的值等於舊的預期值A時才會將內存值V的值修改為B,否則什麽都不幹
偽代碼如下:
if(this.value == A){
this.value = B
return true;
}else{
return false;
}
應用
在java.util.concurrent.atomic包下原子類都是通過CAS來實現的,現在我們以AtomicInteger為例來分析一下CAS的實現
private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value;
Unsafe是CAS的核心類,Java無法直接訪問底層操作系統,而是通過本地(native)方法來訪問。不過盡管如此,JVM還是開了一個後門:Unsafe,它提供了硬件級別的原子操作。
valueOffset為變量值在內存中的偏移地址,unsafe就是通過偏移地址來得到數據的原值的。
value當前值,使用volatile修飾,保證多線程環境下看見的是同一個。
下面我們以AtomicInteger的addAndGet()方法來說明
public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); }
內部調用的是Unsafe類的getAndAddInt()方法
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;
}
而在getAndAddInt方法中又調用了Unsafe的本地方法
public native int getIntVolatile(Object var1, long var2);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
此方法是本地方法(java需要調用其他語言的代碼,比如C語言代碼,跟dll文件有關),有四個參數,分別代表:對象、對象的地址、預期值、修改值
CAS的缺陷
樂觀鎖只能保證一個共享變量的原子操作。如上例子,自旋過程中只能保證value變量的原子性,這時如果多一個或幾個變量,樂觀鎖將變得力不從心,但互斥鎖能輕易解決,不管對象數量多少及對象顆粒度大小。
長時間自旋可能導致開銷大。假如CAS長時間不成功而一直自旋,會給CPU帶來很大的開銷。
ABA問題。CAS的核心思想是通過比對內存值與預期值是否一樣而判斷內存值是否被改過,但這個判斷邏輯不嚴謹,假如內存值原來是A,後來被一條線程改為B,最後又被改成了A,則CAS認為此內存值並沒有發生改變,但實際上是有被其他線程改過的,這種情況對依賴過程值的情景的運算結果影響很大。解決的思路是引入版本號,每次變量更新都把版本號加一。
synchronized與CAS的區別
對於資源競爭較少的情況,使用synchronized同步鎖進行線程阻塞和喚醒切換以及用戶態內核態間的切換操作額外浪費消耗cpu資源;而CAS基於硬件實現,不需要進入內核,不需要切換線程,操作自旋幾率較少,因此可以獲得更高的性能。
對於資源競爭嚴重的情況,CAS自旋的概率會比較大(比如getAndAddInt方法中的do-while循環),從而浪費更多的CPU資源,效率低於synchronized。
java並發之CAS詳解