1. 程式人生 > >什麽是CAS機制?(轉)

什麽是CAS機制?(轉)

原子性操作 利用 理解 興趣 通過 線程 想要 保持 靜態變量

圍繞下面四個點展開敘述:

  一:什麽是CAS機制?

  二:Java當中CAS的底層實現

  三:CAS的ABA問題和解決方法

  四:java8對CAS的優化


一:什麽是CAS機制?

我們先看一段代碼:

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

public class Test4 {
    public static int count =0;
    
    public static void main(String[] args) {
        for(int i = 0; i < 2; i++) {
            
new Thread( new Runnable(){ @Override public void run() { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
for(int j = 0; j < 100; j++) { count++; } }} ).start(); } try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(
"count="+ count); } }

多次console輸出結果:小於200的值

我們再加上synchronized同步鎖,再來看一下。

技術分享圖片

console結果: 200

  加了同步鎖之後,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機制最大的問題所在。(後面有介紹)

二:Java當中CAS的底層實現

  先來一個小例子:i++自增操作

public class CASTest {
    static int i = 0;

    public static void increment() {
        i++;
    }
}

有沒有其他方法來代替 synchronized 對方法的加鎖,並且保證 increment() 方法是線程安全呢?

大家看一下,如果我采用下面這種方式,能否保證 increment 是線程安全的呢?步驟如下:

  1、線程從內存中讀取 i 的值,假如此時 i 的值為 0,我們把這個值稱為 k 吧,即此時 k = 0。

  2、令 j = k + 1。

  3、用 k 的值與內存中i的值相比,如果相等,這意味著沒有其他線程修改過 i 的值,我們就把 j(此時為1) 的值寫入內存;如果不相等(意味著i的值被其他線程修改過),我們就不把j的值寫入內存,而是重新跳回步驟 1,繼續這三個操作。

翻譯成代碼的話就是這樣:

public static void increment() {
    do{
        int k = i;
        int j = k + 1;
    }while (compareAndSet(i, k, j))
}

  如果你去模擬一下,就會發現,這樣寫是線程安全的。

  這裏可能有人會說,第三步的 compareAndSet 這個操作不僅要讀取內存,還幹了比較、寫入內存等操作,,,這一步本身就是線程不安全的啊?

  如果你能想到這個,說明你是真的有去思考、模擬這個過程,不過我想要告訴你的是,這個 compareAndSet 操作,他其實只對應操作系統的一條硬件操作指令,盡管看似有很多操作在裏面,但操作系統能夠保證他是原子執行的。

  對於一條英文單詞很長的指令,我們都喜歡用它的簡稱來稱呼他,所以,我們就把 compareAndSet 稱為 CAS 吧。

  所以,采用 CAS 這種機制的寫法也是線程安全的,通過這種方式,可以說是不存在鎖的競爭,也不存在阻塞等事情的發生,可以讓程序執行的更好。

  在 Java 中,也是提供了這種 CAS 的原子類,例如:

  1. AtomicBoolean
  2. AtomicInteger
  3. AtomicLong
  4. AtomicReference

我們再來看看jdk中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操作之間的原子性操作。

三:CAS的ABA問題和解決方法

我們現在來說什麽是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問題。

四:java8對CAS的優化  

  由於采用這種 CAS 機制是沒有對方法進行加鎖的,所以,所有的線程都可以進入 increment() 這個方法,假如進入這個方法的線程太多,就會出現一個問題:每次有線程要執行第三個步驟的時候,i 的值老是被修改了,所以線程又到回到第一步繼續重頭再來。

  而這就會導致一個問題:由於線程太密集了,太多人想要修改 i 的值了,進而大部分人都會修改不成功,白白著在那裏循環消耗資源。

  為了解決這個問題,Java8 引入了一個 cell[] 數組,它的工作機制是這樣的:假如有 5 個線程要對 i 進行自增操作,由於 5 個線程的話,不是很多,起沖突的幾率較小,那就讓他們按照以往正常的那樣,采用 CAS 來自增吧。

  但是,如果有 100 個線程要對 i 進行自增操作的話,這個時候,沖突就會大大增加,系統就會把這些線程分配到不同的 cell 數組元素去,假如 cell[10] 有 10 個元素吧,且元素的初始化值為 0,那麽系統就會把 100 個線程分成 10 組,每一組對 cell 數組其中的一個元素做自增操作,這樣到最後,cell 數組 10 個元素的值都為 10,系統在把這 10 個元素的值進行匯總,進而得到 100,二這,就等價於 100 個線程對 i 進行了 100 次自增操作。

  當然,我這裏只是舉個例子來說明 Java8 對 CAS 優化的大致原理,具體的大家有興趣可以去看源碼,或者去搜索對應的文章哦。

什麽是CAS機制?(轉)