1. 程式人生 > >(轉載)JAVA中的CAS

(轉載)JAVA中的CAS

=========================================================================================

本篇的思路是先闡明無鎖執行者CAS的核心演算法原理然後分析Java執行CAS的實踐者Unsafe類,該類中的方法都是native修飾的,因此我們會以說明方法作用為主介紹Unsafe類,最後再介紹併發包中的Atomic系統使用CAS原理實現的併發類。

無鎖的概念

      在談論無鎖概念時,總會關聯起樂觀派與悲觀派,對於樂觀派而言,他們認為事情總會往好的方向發展,總是認為壞的情況發生的概率特別小,可以無所顧忌地做事,但對於悲觀派而已,他們總會認為發展事態如果不及時控制,以後就無法挽回了,即使無法挽回的局面幾乎不可能發生。這兩種派系對映到併發程式設計中就如同加鎖與無鎖的策略,即加鎖是一種悲觀策略,無鎖是一種樂觀策略,因為對於加鎖的併發程式來說,它們總是認為每次訪問共享資源時總會發生衝突,因此必須對每一次資料操作實施加鎖策略。而無鎖則總是假設對共享資源的訪問沒有衝突,執行緒可以不停執行,無需加鎖,無需等待,一旦發現衝突,無鎖策略則採用一種稱為CAS的技術來保證執行緒執行的安全性,這項CAS技術就是無鎖策略實現的關鍵,下面我們進一步瞭解CAS技術的奇妙之處。

無鎖的執行者-CAS

CAS

CAS的全稱是Compare And Swap 即比較交換,其演算法核心思想如下

執行函式:CAS(V,E,N)

其包含3個引數

  • V表示要更新的變數

  • E表示預期值

  • N表示新值

       如果V值等於E值,則將V的值設為N。若V值和E值不同,則說明已經有其他執行緒做了更新,則當前執行緒什麼都不做。通俗的理解就是CAS操作需要我們提供一個期望值,當期望值與當前執行緒的變數值相同時,說明還沒執行緒修改該值,當前執行緒可以進行修改,也就是執行CAS操作,但如果期望值與當前執行緒不符,則說明該值已被其他執行緒修改,此時不執行更新操作,但可以選擇重新讀取該變數再嘗試再次修改該變數,也可以放棄操作,原理圖如下

這裡寫圖片描述

      由於CAS操作屬於樂觀派,它總認為自己可以成功完成操作,當多個執行緒同時使用CAS操作一個變數時,只有一個會勝出,併成功更新,其餘均會失敗,但失敗的執行緒並不會被掛起,僅是被告知失敗,並且允許再次嘗試,當然也允許失敗的執行緒放棄操作,這點從圖中也可以看出來。基於這樣的原理,CAS操作即使沒有鎖,同樣知道其他執行緒對共享資源操作影響,並執行相應的處理措施。同時從這點也可以看出,由於無鎖操作中沒有鎖的存在,因此不可能出現死鎖的情況,也就是說無鎖操作天生免疫死鎖。

CPU指令對CAS的支援

      或許我們可能會有這樣的疑問,假設存在多個執行緒執行CAS操作並且CAS的步驟很多,有沒有可能在判斷V和E相同後,正要賦值時,切換了執行緒,更改了值。造成了資料不一致呢?答案是否定的,因為CAS是一種系統原語,原語屬於作業系統用語範疇,是由若干條指令組成的,用於完成某個功能的一個過程,並且原語的執行必須是連續的

,在執行過程中不允許被中斷,也就是說CAS是一條CPU的原子指令,不會造成所謂的資料不一致問題。

鮮為人知的指標: Unsafe類

      Unsafe類存在於sun.misc包中,其內部方法操作可以像C的指標一樣直接操作記憶體,單從名稱看來就可以知道該類是非安全的,畢竟Unsafe擁有著類似於C的指標操作,因此總是不應該首先使用Unsafe類,Java官方也不建議直接使用的Unsafe類,但我們還是很有必要了解該類,因為Java中CAS操作的執行依賴於Unsafe類的方法,注意Unsafe類中的所有方法都是native修飾的,也就是說Unsafe類中的方法都直接呼叫作業系統底層資源執行相應任務,關於Unsafe類的主要功能點如下:

記憶體管理,Unsafe類中存在直接操作記憶體的方法;

//分配記憶體指定大小的記憶體

public native long allocateMemory(long bytes);

//根據給定的記憶體地址address設定重新分配指定大小的記憶體

public native long reallocateMemory(long address, long bytes);

//用於釋放allocateMemory和reallocateMemory申請的記憶體

public native void freeMemory(long address);

//將指定物件的給定offset偏移量記憶體塊中的所有位元組設定為固定值

public native void setMemory(Object o, long offset, long bytes, byte value);

//設定給定記憶體地址的值

public native void putAddress(long address, long x);

//獲取指定記憶體地址的值

public native long getAddress(long address);

//設定給定記憶體地址的long值

public native void putLong(long address, long x);

//獲取指定記憶體地址的long值

public native long getLong(long address);

//設定或獲取指定記憶體的byte值

//其他基本資料型別(long,char,float,double,short等)的操作與putByte及getByte相同

public native byte getByte(long address);

public native void putByte(long address, byte x);

//作業系統的記憶體頁大小

public native int pageSize();

提供例項物件新途徑:

//傳入一個物件的class並建立該例項物件,但不會呼叫構造方法

public native Object allocateInstance(Class cls) throws InstantiationException;

類和例項物件以及變數的操作,就不貼出來了

//傳入Field f,獲取欄位f在例項物件中的偏移量

//獲得給定物件偏移量上的int值,所謂的偏移量可以簡單理解為指標指向該變數的記憶體地址,

//通過偏移量便可得到該物件的變數

//通過偏移量可以設定給定物件上偏移量的int值

//獲得給定物件偏移量上的引用型別的值

//通過偏移量可以設定給定物件偏移量上的引用型別的值

雖然在Unsafe類中存在getUnsafe()方法,但該方法只提供給高階的Bootstrap類載入器使用,普通使用者呼叫將丟擲異常,所以我們在Demo中使用了反射技術獲取了Unsafe例項物件並進行相關操作。

Unsafe裡的CAS 操作相關 

CAS是一些CPU直接支援的指令,也就是我們前面分析的無鎖操作,在Java中無鎖操作CAS基於以下3個方法實現,在稍後講解Atomic系列內部方法是基於下述方法的實現的。

//第一個引數o為給定物件,offset為物件記憶體的偏移量,通過這個偏移量迅速定位欄位並設定或獲取該欄位的值,

//expected表示期望值,x表示要設定的值,下面3個方法都通過CAS原子指令執行操作。

public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);

public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);

public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

掛起與恢復 
將一個執行緒進行掛起是通過park方法實現的,呼叫 park後,執行緒將一直阻塞直到超時或者中斷等條件出現。unpark可以終止一個掛起的執行緒,使其恢復正常。Java對執行緒的掛起操作被封裝在 LockSupport類中,LockSupport類中有各種版本pack方法,其底層實現最終還是使用Unsafe.park()方法和Unsafe.unpark()方法

//執行緒呼叫該方法,執行緒將一直阻塞直到超時,或者是中斷條件出現。

public native void park(boolean isAbsolute, long time);

//終止掛起的執行緒,恢復正常.java.util.concurrent包中掛起操作都是在LockSupport類實現的,其底層正是使用這兩個方法,

public native void unpark(Object thread);

併發包中的原子操作類(Atomic系列)

通過前面的分析我們已基本理解了無鎖CAS的原理並對Java中的指標類Unsafe類有了比較全面的認識,下面進一步分析CAS在Java中的應用,即併發包中的原子操作類(Atomic系列),從JDK 1.5開始提供了java.util.concurrent.atomic包,在該包中提供了許多基於CAS實現的原子操作類,用法方便,效能高效,主要分以下4種類型。

原子更新基本型別

原子更新基本型別主要包括3個類:

  • AtomicBoolean:原子更新布林型別
  • AtomicInteger:原子更新整型
  • AtomicLong:原子更新長整型

這3個類的實現原理和使用方式幾乎是一樣的,這裡我們以AtomicInteger為例進行分析,AtomicInteger主要是針對int型別的資料執行原子操作,它提供了原子自增方法、原子自減方法以及原子賦值方法等,鑑於AtomicInteger的原始碼不多,我們直接看原始碼

public class AtomicInteger extends Number implements java.io.Serializable {

private static final long serialVersionUID = 6214790243416807050L;



// 獲取指標類Unsafe

private static final Unsafe unsafe = Unsafe.getUnsafe();



//下述變數value在AtomicInteger例項物件內的記憶體偏移量

private static final long valueOffset;



static {

try {

//通過unsafe類的objectFieldOffset()方法,獲取value變數在物件記憶體中的偏移

//通過該偏移量valueOffset,unsafe類的內部方法可以獲取到變數value對其進行取值或賦值操作

valueOffset = unsafe.objectFieldOffset

(AtomicInteger.class.getDeclaredField("value"));

} catch (Exception ex) { throw new Error(ex); }

}

//當前AtomicInteger封裝的int變數value

private volatile int value;



public AtomicInteger(int initialValue) {

value = initialValue;

}

public AtomicInteger() {

}

//獲取當前最新值,

public final int get() {

return value;

}

//設定當前值,具備volatile效果,方法用final修飾是為了更進一步的保證執行緒安全。

public final void set(int newValue) {

value = newValue;

}

//最終會設定成newValue,使用該方法後可能導致其他執行緒在之後的一小段時間內可以獲取到舊值,有點類似於延遲載入

public final void lazySet(int newValue) {

unsafe.putOrderedInt(this, valueOffset, newValue);

}

//設定新值並獲取舊值,底層呼叫的是CAS操作即unsafe.compareAndSwapInt()方法

public final int getAndSet(int newValue) {

return unsafe.getAndSetInt(this, valueOffset, newValue);

}

//如果當前值為expect,則設定為update(當前值指的是value變數)

public final boolean compareAndSet(int expect, int update) {

return unsafe.compareAndSwapInt(this, valueOffset, expect, update);

}

//當前值加1返回舊值,底層CAS操作

public final int getAndIncrement() {

return unsafe.getAndAddInt(this, valueOffset, 1);

}

//當前值減1,返回舊值,底層CAS操作

public final int getAndDecrement() {

return unsafe.getAndAddInt(this, valueOffset, -1);

}

//當前值增加delta,返回舊值,底層CAS操作

public final int getAndAdd(int delta) {

return unsafe.getAndAddInt(this, valueOffset, delta);

}

//當前值加1,返回新值,底層CAS操作

public final int incrementAndGet() {

return unsafe.getAndAddInt(this, valueOffset, 1) + 1;

}

//當前值減1,返回新值,底層CAS操作

public final int decrementAndGet() {

return unsafe.getAndAddInt(this, valueOffset, -1) - 1;

}

//當前值增加delta,返回新值,底層CAS操作

public final int addAndGet(int delta) {

return unsafe.getAndAddInt(this, valueOffset, delta) + delta;

}

//省略一些不常用的方法....

}

通過上述的分析,可以發現AtomicInteger原子類的內部幾乎是基於前面分析過Unsafe類中的CAS相關操作的方法實現的,這也同時證明AtomicInteger是基於無鎖實現的,這裡重點分析自增操作實現過程,其他方法自增實現原理一樣。

我們發現AtomicInteger類中所有自增或自減的方法都間接呼叫Unsafe類中的getAndAddInt()方法實現了CAS操作,從而保證了執行緒安全,關於getAndAddInt其實前面已分析過,它是Unsafe類中1.8新增的方法,原始碼如下

//Unsafe類中的getAndAddInt方法

public final int getAndAddInt(Object o, long offset, int delta) {

int v;

do {

v = getIntVolatile(o, offset);

} while (!compareAndSwapInt(o, offset, v, v + delta));

return v;

}

可看出getAndAddInt通過一個while迴圈不斷的重試更新要設定的值,直到成功為止,呼叫的是Unsafe類中的compareAndSwapInt方法,是一個CAS操作方法。這裡需要注意的是,上述原始碼分析是基於JDK1.8的,如果是1.8之前的方法,AtomicInteger原始碼實現有所不同,是基於for死迴圈的,如下

//JDK 1.7的原始碼,由for的死迴圈實現,並且直接在AtomicInteger實現該方法,

//JDK1.8後,該方法實現已移動到Unsafe類中,直接呼叫getAndAddInt方法即可

public final int incrementAndGet() {

for (;;) {

int current = get();

int next = current + 1;

if (compareAndSet(current, next))

return next;

}

}

CAS的ABA問題及其解決方案

假設這樣一種場景,當第一個執行緒執行CAS(V,E,U)操作,在獲取到當前變數V,準備修改為新值U前,另外兩個執行緒已連續修改了兩次變數V的值,使得該值又恢復為舊值,這樣的話,我們就無法正確判斷這個變數是否已被修改過,如下圖

這就是典型的CAS的ABA問題,一般情況這種情況發現的概率比較小,可能發生了也不會造成什麼問題,比如說我們對某個做加減法,不關心數字的過程,那麼發生ABA問題也沒啥關係。但是在某些情況下還是需要防止的,那麼該如何解決呢?在Java中解決ABA問題,我們可以使用以下兩個原子類

AtomicStampedReference類

  • AtomicStampedReference原子類是一個帶有時間戳的物件引用,在每次修改後,AtomicStampedReference不僅會設定新值而且還會記錄更改的時間。當AtomicStampedReference設定物件值時,物件值以及時間戳都必須滿足期望值才能寫入成功,這也就解決了反覆讀寫時,無法預知值是否已被修改的窘境

底層實現為:通過Pair私有內部類儲存資料和時間戳, 並構造volatile修飾的私有例項

接著看AtomicStampedReference類的compareAndSet()方法的實現:

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)));
    }

同時對當前資料和當前時間進行比較,只有兩者都相等是才會執行casPair()方法,

單從該方法的名稱就可知是一個CAS方法,最終呼叫的還是Unsafe類中的compareAndSwapObject方法

到這我們就很清晰AtomicStampedReference的內部實現思想了,

通過一個鍵值對Pair儲存資料和時間戳,在更新時對資料和時間戳進行比較,

只有兩者都符合預期才會呼叫Unsafe的compareAndSwapObject方法執行數值和時間戳替換,也就避免了ABA的問題。

AtomicMarkableReference類

AtomicMarkableReference與AtomicStampedReference不同的是,

AtomicMarkableReference維護的是一個boolean值的標識,也就是說至於true和false兩種切換狀態,

經過博主測試,這種方式並不能完全防止ABA問題的發生,只能減少ABA問題發生的概率。

AtomicMarkableReference的實現原理與AtomicStampedReference類似,這裡不再介紹。到此,我們也明白瞭如果要完全杜絕ABA問題的發生,我們應該使用AtomicStampedReference原子類更新物件,而對於AtomicMarkableReference來說只能減少ABA問題的發生概率,並不能杜絕。

再談自旋鎖

自旋鎖是一種假設在不久將來,當前的執行緒可以獲得鎖,因此虛擬機器會讓當前想要獲取鎖的執行緒做幾個空迴圈(這也是稱為自旋的原因),在經過若干次迴圈後,如果得到鎖,

就順利進入臨界區。如果還不能獲得鎖,那就會將執行緒在作業系統層面掛起,這種方式確實也是可以提升效率的。但問題是當執行緒越來越多競爭很激烈時,

佔用CPU的時間變長會導致效能急劇下降,因此Java虛擬機器內部一般對於自旋鎖有一定的次數限制,可能是50或者100次迴圈後就放棄,直接掛起執行緒,讓出CPU資源。

如下通過AtomicReference可實現簡單的自旋鎖。

public class SpinLock {

private AtomicReference<Thread> sign =new AtomicReference<>();



public void lock(){

Thread current = Thread.currentThread();

while(!sign .compareAndSet(null, current)){

}

}



public void unlock (){

Thread current = Thread.currentThread();

sign .compareAndSet(current, null);

}

}

使用CAS原子操作作為底層實現,lock()方法將要更新的值設定為當前執行緒,並將預期值設定為null。unlock()函式將要更新的值設定為null,並預期值設定為當前執行緒。然後我們通過lock()和unlock來控制自旋鎖的開啟與關閉,注意這是一種非公平鎖。事實上AtomicInteger(或者AtomicLong)原子類內部的CAS操作也是通過不斷的自迴圈(while迴圈)實現,不過這種迴圈的結束條件是執行緒成功更新對於的值,但也是自旋鎖的一種。

public class SpinLock {

private AtomicReference<Thread> sign =new AtomicReference<>();



public void lock(){

Thread current = Thread.currentThread();

while(!sign .compareAndSet(null, current)){

}

}



public void unlock (){

Thread current = Thread.currentThread();

sign .compareAndSet(current, null);

}

}

使用CAS原子操作作為底層實現,lock()方法將要更新的值設定為當前執行緒,並將預期值設定為null。unlock()函式將要更新的值設定為null,並預期值設定為當前執行緒。然後我們通過lock()和unlock來控制自旋鎖的開啟與關閉,注意這是一種非公平鎖。事實上AtomicInteger(或者AtomicLong)原子類內部的CAS操作也是通過不斷的自迴圈(while迴圈)實現,不過這種迴圈的結束條件是執行緒成功更新對於的值,但也是自旋鎖的一種。