標籤:shm 地址 多線程 進入 volatile ctf 語言 更新 cal
前言
在高並發的應用當中,最關鍵的問題就是對共用變數的安全訪問,通常我們都是通過加鎖的方式,比如說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詳解