Java併發之CAS與原子類實現原理講解

LiQiyaoo發表於2018-02-10

一、CAS是什麼?

CAS的全稱是Compare and Swap,即比較並交換。比較的是當前記憶體中儲存的值與預期原值,交換的是新值與記憶體中的值。這個操作是硬體層面的指令,因此能夠保證原子性。Java通過JNI(本地方法呼叫)來使用這個原子操作,也是樂觀鎖最常用的機制。

CAS操作包含三個運算元——記憶體位置、預期原值和新值。在執行CAS操作時,先進行Compare操作,即比較記憶體位置的值與預期原值是否相等,若相等,則執行Swap操作將新值放入該記憶體位置。若不相等,則不進行Swap操作。

這時你一定會想,為什麼要這樣做,預期值和原值究竟從哪來?

我將通過一個例子講解,下面先考慮一下併發情況下的自增操作即如何實現。

如果把自增直接寫成i++,那一定會出現併發問題,因為這不是原子操作,就不多說了。但是有了CAS操作之後,併發環境下的自增操作就可以很安全的實現了。下面來看一下如何藉助CAS原子操作實現自增操作,先看一段虛擬碼。

        //自旋直到CAS操作成功
        do{
            oldValue = getCurrent(addr);//在執行CAS之前獲取預期原值
            newValue = oldValue + 1;//根據預期原值做增加操作的到新值
        }while (!compareAndSwap(addr, oldValue, newValue));//執行CAS操作

(1)首先獲得預期原值,因為在自增情況下,新值是依賴於舊值的。

(2)通過計算得到新值。在這個過程中,可能有其他執行緒對該記憶體位置的值進行更新(自增),因為我們採用樂觀鎖的概念,並沒有對變數進行加鎖。

(3)再執行CAS操作。首先比較預期原值與當前記憶體位置的值是否相等。若相同,說明在這期間,沒有其他執行緒對該變數進行更新,沒有併發問題發生,則可以執行swap操作對舊值進行更新;若不同,則說明在這期間,有其他執行緒對變數進行了更新,當前的newValue其實是失效的,則要重新執行迴圈,即自旋,直至更新成功。

二、原子類AtomicInteger

原子類的自增、加法等操作底層都是通過自旋CAS操作實現的,其核心原理就是我上面寫的虛擬碼。

下面來看一下原始碼:

private static final Unsafe unsafe = Unsafe.getUnsafe();//最重要的Unsafe類,其中有對底層CAS操作的封裝的方法
    private static final long valueOffset;//value相對於物件在記憶體中的偏移量,在執行CAS操作時需要用到
    static {//初始化求偏移量
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
public final int getAndIncrement() {//自增操作
        return unsafe.getAndAddInt(this, valueOffset, 1);//呼叫Unsafe物件的方法
}
UnSafe類中的重要方法:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

這些方法都是native的,是呼叫了底層作業系統的CAS指令。


三、CAS操作的缺點

1、ABA。value初值為A,執行緒1讀取到預期原值為A,執行緒2讀取到預期原值為A,執行緒1通過CAS操作將A更新為B,再通過一次CAS操作將值更新為A,此時執行緒2進行CAS操作,在比較時發現預期原值A與當前記憶體位置的值A相同,則進行更新,但是此時value已經被更新過了,而不是原來那個A值了,這樣會產生執行緒安全問題。但是現在也有解決方案,在Java API中有一個類為AtomicStampedReference,該類在記錄預期原值的同時還會記錄標誌,在比較時還會比較標誌。

2、自旋時間長的情況下會導致很大開銷,若JVM支援pause指令,則可以解決此問題。pause指令有兩個作用,第一是延遲流水線執行指令,使得CPU不會消耗太多執行時間;第二是避免在退出迴圈時因記憶體順序衝突而引起CPU流水線被清空。

四、CAS與volatile

在Java的concurrent包中,有一種通用的實現方式,即CAS配合volatile來實現許多高併發類。

一般情況下實現流程:

(1)宣告變數為volatile

(2)使用CAS條件更新來實現執行緒之間的同步。

(3)使用volatile變數的讀/寫和CAS所具有的volatile讀和寫的記憶體語義來實現執行緒之間的通訊。

Java併發包的框架如圖所示:

                            (圖片來自網路)

相關文章