什麼是CAS機制?

轉載聲明:無法找到原作者,查到後會添加聲明

我們先看一段代碼:

啓動兩個線程,每個線程中讓靜態變量count循環累加100次。

最終輸出的count結果一定是200嗎?因爲這段代碼是非線程安全的,所以最終的自增結果很可能會小於200。我們再加上synchronized同步鎖,再來看一下。

加了同步鎖之後,count自增的操作變成了原子性操作,所以最終輸出一定是count=200,代碼實現了線程安全。雖然synchronized確保了線程安全,但是在某些情況下,這並不是一個最有的選擇。

關鍵在於性能問題。

synchronized關鍵字會讓沒有得到鎖資源的線程進入BLOCKED狀態,而後在爭奪到鎖資源後恢復爲RUNNABLE狀態,這個過程中涉及到操作系統用戶模式和內核模式的轉換,代價比較高。

儘管JAVA 1.6爲synchronized做了優化,增加了從偏向鎖到輕量級鎖再到重量級鎖的過過度,但是在最終轉變爲重量級鎖之後,性能仍然比較低。所以面對這種情況,我們就可以使用java中的“原子操作類”。

所謂原子操作類,指的是java.util.concurrent.atomic包下,一系列以Atomic開頭的包裝類。如AtomicBoolean,AtomicUInteger,AtomicLong。它們分別用於Boolean,Integer,Long類型的原子性操作。

現在我們嘗試使用AtomicInteger類:

使用AtomicInteger之後,最終的輸出結果同樣可以保證是200。並且在某些情況下,代碼的性能會比synchronized更好。

而Atomic操作類的底層正是用到了“CAS機制”。

CAS是英文單詞Compare and Swap的縮寫,翻譯過來就是比較並替換。

CAS機制中使用了3個基本操作數:內存地址V,舊的預期值A,要修改的新值B。

更新一個變量的時候,只有當變量的預期值A和內存地址V當中的實際值相同時,纔會將內存地址V對應的值修改爲B。

我們看一個例子:

1. 在內存地址V當中,存儲着值爲10的變量。

2. 此時線程1想把變量的值增加1.對線程1來說,舊的預期值A=10,要修改的新值B=11.

3. 在線程1要提交更新之前,另一個線程2搶先一步,把內存地址V中的變量值率先更新成了11。

4. 線程1開始提交更新,首先進行A和地址V的實際值比較,發現A不等於V的實際值,提交失敗。

5. 線程1 重新獲取內存地址V的當前值,並重新計算想要修改的值。此時對線程1來說,A=11,B=12。這個重新嘗試的過程被稱爲自旋。

6. 這一次比較幸運,沒有其他線程改變地址V的值。線程1進行比較,發現A和地址V的實際值是相等的。

7. 線程1進行交換,把地址V的值替換爲B,也就是12.

從思想上來說,synchronized屬於悲觀鎖,悲觀的認爲程序中的併發情況嚴重,所以嚴防死守,CAS屬於樂觀鎖,樂觀地認爲程序中的併發情況不那麼嚴重,所以讓線程不斷去重試更新。

在java中除了上面提到的Atomic系列類,以及Lock系列類奪得底層實現,甚至在JAVA1.6以上版本,synchronized轉變爲重量級鎖之前,也會採用CAS機制。

CAS的缺點:

1) CPU開銷過大

在併發量比較高的情況下,如果許多線程反覆嘗試更新某一個變量,卻又一直更新不成功,循環往復,會給CPU帶來很到的壓力。

2) 不能保證代碼塊的原子性

CAS機制所保證的知識一個變量的原子性操作,而不能保證整個代碼塊的原子性。比如需要保證3個變量共同進行原子性的更新,就不得不使用synchronized了。

3) ABA問題

這是CAS機制最大的問題所在。(後面有介紹)

 

我們下面來介紹一下兩個問題:

1. JAVA中CAS的底層實現

2. CAS的ABA問題和解決辦法。

我們看一下AtomicInteger當中常用的自增方法incrementAndGet:

public final int incrementAndGet() {

    for (;;) {

        int current = get();

        int next = current + 1;

        if (compareAndSet(current, next))

            return next;

    }

}

private volatile int value; 

public final int get() {

    return value;

}

這段代碼是一個無限循環,也就是CAS的自旋,循環體中做了三件事:

1. 獲取當前值

2. 當前值+1,計算出目標值

3. 進行CAS操作,如果成功則跳出循環,如果失敗則重複上述步驟

這裏需要注意的重點是get方法,這個方法的作用是獲取變量的當前值。

如何保證獲取的當前值是內存中的最新值?很簡單,用volatile關鍵字來保證(保證線程間的可見性)。我們接下來看一下compareAndSet方法的實現:

compareAndSet方法的實現很簡單,只有一行代碼。這裏涉及到兩個重要的對象,一個是unsafe,一個是valueOffset。

什麼是unsafe呢?Java語言不像C,C++那樣可以直接訪問底層操作系統,但是JVM爲我們提供了一個後門,這個後門就是unsafe。unsafe爲我們提供了硬件級別的原子操作。

至於valueOffset對象,是通過unsafe.objectFiledOffset方法得到,所代表的是AtomicInteger對象value成員變量在內存中的偏移量。我們可以簡單的把valueOffset理解爲value變量的內存地址。

我們上面說過,CAS機制中使用了3個基本操作數:內存地址V,舊的預期值A,要修改的新值B。

而unsafe的compareAndSwapInt方法的參數包括了這三個基本元素:valueOffset參數代表了V,expect參數代表了A,update參數代表了B。

正是unsafe的compareAndSwapInt方法保證了Compare和Swap操作之間的原子性操作。

我們現在來說什麼是ABA問題。假設內存中有一個值爲A的變量,存儲在地址V中。

此時有三個線程想使用CAS的方式更新這個變量的值,每個線程的執行時間有略微偏差。線程1和線程2已經獲取當前值,線程3還未獲取當前值。

接下來,線程1先一步執行成功,把當前值成功從A更新爲B;同時線程2因爲某種原因被阻塞住,沒有做更新操作;線程3在線程1更新之後,獲取了當前值B。

在之後,線程2仍然處於阻塞狀態,線程3繼續執行,成功把當前值從B更新成了A。

最後,線程2終於恢復了運行狀態,由於阻塞之前已經獲得了“當前值A”,並且經過compare檢測,內存地址V中的實際值也是A,所以成功把變量值A更新成了B。

看起來這個例子沒啥問題,但如果結合實際,就可以發現它的問題所在。

我們假設一個提款機的例子。假設有一個遵循CAS原理的提款機,小灰有100元存款,要用這個提款機來提款50元。

由於提款機硬件出了點問題,小灰的提款操作被同時提交了兩次,開啓了兩個線程,兩個線程都是獲取當前值100元,要更新成50元。

理想情況下,應該一個線程更新成功,一個線程更新失敗,小灰的存款值被扣一次。

線程1首先執行成功,把餘額從100改成50.線程2因爲某種原因阻塞。這時,小灰的媽媽剛好給小灰匯款50元。

線程2仍然是阻塞狀態,線程3執行成功,把餘額從50改成了100。

線程2恢復運行,由於阻塞之前獲得了“當前值”100,並且經過compare檢測,此時存款實際值也是100,所以會成功把變量值100更新成50。

原本線程2應當提交失敗,小灰的正確餘額應該保持100元,結果由於ABA問題提交成功了。

怎麼解決呢?加個版本號就可以了。

真正要做到嚴謹的CAS機制,我們在compare階段不僅要比較期望值A和地址V中的實際值,還要比較變量的版本號是否一致。

我們仍然以剛纔的例子來說明,假設地址V中存儲着變量值A,當前版本號是01。線程1獲取了當前值A和版本號01,想要更新爲B,但是被阻塞了。

這時候,內存地址V中變量發生了多次改變,版本號提升爲03,但是變量值仍然是A。

隨後線程1恢復運行,進行compare操作。經過比較,線程1所獲得的值和地址的實際值都是A,但是版本號不相等,所以這一次更新失敗。

 

在Java中,AtomicStampedReference類就實現了用版本號作比較額CAS機制。

 

1. java語言CAS底層如何實現?

利用unsafe提供的原子性操作方法。

2.什麼事ABA問題?怎麼解決?

當一個值從A變成B,又更新回A,普通CAS機制會誤判通過檢測。

利用版本號比較可以有效解決ABA問題。

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