源碼閱讀: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();

雖然在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()方法的實現:

同時對當前數據和當前時間進行比較,只有兩者都相等是纔會執行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循環)實現,不過這種循環的結束條件是線程成功更新對於的值,但也是自旋鎖的一種。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章