天天看點

JAVA-CAS 了解

1.悲觀鎖

示例程式:啟動兩個線程,每個線程中讓靜态變量count循環累加100次。

JAVA-CAS 了解

最終輸出的count結果是什麼呢?一定會是200嗎?

JAVA-CAS 了解
加了同步鎖之後,count自增的操作變成了原子性操作,是以最終的輸出一定是count=200,代碼實作了線程安全。

Synchronized Lock悲觀鎖

為什麼這麼說呢?關鍵在于性能問題。

Synchronized關鍵字會讓沒有得到鎖資源的線程進入

BLOCKED

狀态,而後在争奪到鎖資源後恢複為

RUNNABLE

狀态,這個過程中涉及到作業系統使用者模式和核心模式的轉換,代價比較高。

盡管Java1.6為

Synchronized

做了優化,增加了從偏向鎖到輕量級鎖再到重量級鎖的過度,但是在最終轉變為重量級鎖之後,性能仍然較低。

2.更高效的方案:AtomicXXXX 原子類

所謂原子操作類,指的是

java.util.concurrent.atomic

包下,一系列以

Atomic

開頭的包裝類。例如

AtomicBoolean

AtomicInteger

AtomicLong

。它們分别用于

Boolean

Integer

Long

類型的原子性操作。

現在我們嘗試在代碼中引入

AtomicInteger

類:

JAVA-CAS 了解

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

3.什麼是CAS?

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

CAS機制當中使用了3個基本操作數:記憶體位址V,舊的預期值A,要修改的新值B。

更新一個變量的時候,隻有當變量的預期值A和記憶體位址V當中的實際值相同時,才會将記憶體位址V對應的值修改為B。

這樣說或許有些抽象,我們來看一個例子:

1.在記憶體位址V當中,存儲着值為10的變量。

JAVA-CAS 了解

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

JAVA-CAS 了解

3.線上程1要送出更新之前,另一個線程2搶先一步,把記憶體位址V中的變量值率先更新成了11。

JAVA-CAS 了解

4.線程1開始送出更新,首先進行A和位址V的實際值比較(Compare),發現A不等于V的實際值,送出失敗。

JAVA-CAS 了解

5.線程1重新擷取記憶體位址V的目前值,并重新計算想要修改的新值。此時對線程1來說,A=11,B=12。這個重新嘗試的過程被稱為自旋。

JAVA-CAS 了解

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

JAVA-CAS 了解

7.線程1進行SWAP,把位址V的值替換為B,也就是12。

JAVA-CAS 了解

從思想上來說,

Synchronized屬于悲觀鎖

,悲觀地認為程式中的并發情況嚴重,是以嚴防死守。

CAS屬于樂觀鎖

,樂觀地認為程式中的并發情況不那麼嚴重,是以讓線程不斷去嘗試更新。

4.CAS的缺點

  • CPU開銷較大

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

  • 不能保證代碼塊的原子性

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

  • ABA問題

這是CAS機制最大的問題所在。

什麼是ABA問題?怎麼解決?

5.CAS實作

首先看一看

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方法的實作,以及方法所依賴對象的來曆:

JAVA-CAS 了解

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

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

至于valueOffset對象,是通過unsafe.objectFieldOffset方法得到,所代表的是AtomicInteger對象value成員變量在記憶體中的偏移量。我們可以簡單地把valueOffset了解為value變量的記憶體位址。

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

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

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

6.ABA問題

什麼是ABA呢?假設記憶體中有一個值為A的變量,存儲在位址V當中。

JAVA-CAS 了解

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

JAVA-CAS 了解

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

JAVA-CAS 了解

再之後,線程2仍然處于阻塞狀态,線程3繼續執行,成功把目前值從B更新成了A。

JAVA-CAS 了解

最後,線程2終于恢複了運作狀态,由于阻塞之前已經獲得了“目前值”A,并且經過compare檢測,記憶體位址V中的實際值也是A,是以成功把變量值A更新成了B。

JAVA-CAS 了解

這個過程中,線程2擷取到的變量值A是一個舊值,盡管和目前的實際值相同,但記憶體位址V中的變量已經經曆了A->B->A的改變。

7.ABA問題解決

什麼意思呢?真正要做到嚴謹的CAS機制,我們在Compare階段不僅要比較期望值A和位址V中的實際值,還要比較變量的版本号是否一緻。

我們仍然以最初的例子來說明一下,假設位址V中存儲着變量值A,目前版本号是01。線程1獲得了目前值A和版本号01,想要更新為B,但是被阻塞了。

JAVA-CAS 了解

這時候,記憶體位址V中的變量發生了多次改變,版本号提升為03,但是變量值仍然是A。

JAVA-CAS 了解

随後線程1恢複運作,進行Compare操作。經過比較,線程1所獲得的值和位址V的實際值都是A,但是版本号不相等,是以這一次更新失敗。

JAVA-CAS 了解

在Java當中,AtomicStampedReference類就實作了用版本号做比較的CAS機制。