1. 程式人生 > >Java多執行緒系列---“JUC原子類”01之 原子類的實現(CAS演算法)

Java多執行緒系列---“JUC原子類”01之 原子類的實現(CAS演算法)

轉自:https://blog.csdn.net/ls5718/article/details/52563959  & https://blog.csdn.net/mmoren/article/details/79185862(含部分修改)

 

在JDK 5之前Java語言是靠synchronized關鍵字保證同步的,這會導致有鎖

鎖機制存在以下問題

(1)在多執行緒競爭下,加鎖、釋放鎖會導致比較多的上下文切換和排程延時,引起效能問題。

(2)一個執行緒持有鎖會導致其它所有需要此鎖的執行緒掛起。

(3)如果一個優先順序高的執行緒等待一個優先順序低的執行緒釋放鎖會導致優先順序倒置,引起效能風險。

volatile是不錯的機制,但是volatile不能保證原子性。因此對於同步最終還是要回到鎖機制上來。

獨佔鎖是一種悲觀鎖,synchronized就是一種獨佔鎖,會導致其它所有需要鎖的執行緒掛起,等待持有鎖的執行緒釋放鎖。而另一個更加有效的鎖就是樂觀鎖。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因為衝突失敗就重試,直到成功為止。樂觀鎖用到的機制就是CAS,Compare and Swap。

 

一、什麼是CAS

CAS,compare and swap的縮寫,中文翻譯成比較並交換。

在Java發展初期,java語言是不能夠利用硬體提供的這些便利來提升系統的效能的。而隨著java不斷的發展,Java本地方法(JNI)的出現,使得java程式越過JVM直接呼叫本地方法提供了一種便捷的方式,因而java在併發的手段上也多了起來。而在Doug Lea提供的cucurenct包中,CAS理論是它實現整個java包的基石。

CAS 操作包含三個運算元 —— 記憶體位置(V)、預期原值(A)和新值(B)。 如果記憶體位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新為新值 。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該 位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前 值。)CAS 有效地說明了“我認為位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。”

通常將 CAS 用於同步的方式是從地址 V 讀取值 A,執行多步計算來獲得新 值 B,然後使用 CAS 將 V 的值從 A 改為 B。如果 V 處的值尚未同時更改,則 CAS 操作成功。

類似於 CAS 的指令允許演算法執行讀-修改-寫操作,而無需害怕其他執行緒同時 修改變數,因為如果其他執行緒修改變數,那麼 CAS 會檢測它(並失敗),演算法 可以對該操作重新計算。

 

二、CAS的目的

利用CPU的CAS指令,同時藉助JNI來完成Java的非阻塞演算法。其它原子操作都是利用類似的特性完成的。整個J.U.C都是建立在CAS之上的,因此對於synchronized阻塞演算法,J.U.C在效能上有了很大的提升。

三、CAS存在的問題

CAS雖然很高效的解決原子操作,但是CAS仍然存在三大問題。ABA問題,迴圈時間長開銷大和只能保證一個共享變數的原子操作

1.  ABA問題。因為CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變數前面追加上版本號,每次變數更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。

從Java1.5開始JDK的atomic包裡提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式(最終呼叫的是Unsafe中的方法)將該引用和該標誌的值設定為給定的更新值。

關於ABA問題參考文件: http://blog.hesey.net/2011/09/resolve-aba-by-atomicstampedreference.html

2. 迴圈時間長開銷大。自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM能支援處理器提供的pause指令那麼效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出迴圈的時候因記憶體順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。

 

3. 只能保證一個共享變數的原子操作。當對一個共享變數執行操作時,我們可以使用迴圈CAS的方式來保證原子操作,但是對多個共享變數操作時,迴圈CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變數合併成一個共享變數來操作。比如有兩個共享變數i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用物件之間的原子性,你可以把多個變數放在一個物件裡來進行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類中的方法都直接呼叫作業系統底層資源執行相應任務。
1. 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);

2. 掛起與恢復

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

六、concurrent包的實現

由於java的CAS同時具有 volatile 讀和volatile寫的記憶體語義,因此Java執行緒之間的通訊現在有了下面四種方式:

  1. A執行緒寫volatile變數,隨後B執行緒讀這個volatile變數。
  2. A執行緒寫volatile變數,隨後B執行緒用CAS更新這個volatile變數。
  3. A執行緒用CAS更新一個volatile變數,隨後B執行緒用CAS更新這個volatile變數。
  4. A執行緒用CAS更新一個volatile變數,隨後B執行緒讀這個volatile變數。

Java的CAS會使用現代處理器上提供的高效機器級別原子指令,這些原子指令以原子方式對記憶體執行讀-改-寫操作,這是在多處理器中實現同步的關鍵(從本質上來說,能夠支援原子性讀-改-寫指令的計算機器,是順序計算圖靈機的非同步等價機器,因此任何現代的多處理器都會去支援某種能對記憶體執行原子性讀-改-寫操作的原子指令)。同時,volatile變數的讀/寫和CAS可以實現執行緒之間的通訊。把這些特性整合在一起,就形成了整個concurrent包得以實現的基石。如果我們仔細分析concurrent包的原始碼實現,會發現一個通用化的實現模式:

  1. 首先,宣告共享變數為volatile;
  2. 然後,使用CAS的原子條件更新來實現執行緒之間的同步;
  3. 同時,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的記憶體語義來實現執行緒之間的通訊。

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

原子更新基本型別主要包括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;
    }
}

2. CAS中的ABA問題及其解決方案

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

 

 

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

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

底層實現為: 通過Pair私有內部類儲存資料和時間戳, 並構造volatile修飾的私有例項
接著看AtomicStampedReference類的compareAndSet()方法的實現:

 

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

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

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

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

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

(2)AtomicMarkableReference類

 

AtomicMarkableReference與AtomicStampedReference不同的是,

 

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

 

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

 

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