天天看點

《我要進大廠系列 二》-說說你對CAS了解

文章目錄

  • ​​1.場景引入,問題凸現​​
  • ​​2.更高效的方案:AtomicXXXX 原子類​​
  • ​​3.什麼是CAS?​​
  • ​​4.CAS的缺點​​
  • ​​5.遺留的兩個問題:​​
  • ​​6.ABA問題​​
  • ​​7.ABA問題解決​​
  • ​​8.小結​​
  • ​​8.面試回答​​

1.場景引入,問題凸現

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

《我要進大廠系列 二》-說說你對CAS了解

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

《我要進大廠系列 二》-說說你對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​

​​類:

《我要進大廠系列 二》-說說你對CAS了解

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

3.什麼是CAS?

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

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

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

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

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

《我要進大廠系列 二》-說說你對CAS了解

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

《我要進大廠系列 二》-說說你對CAS了解

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

《我要進大廠系列 二》-說說你對CAS了解

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

《我要進大廠系列 二》-說說你對CAS了解

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

《我要進大廠系列 二》-說說你對CAS了解

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

《我要進大廠系列 二》-說說你對CAS了解

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

《我要進大廠系列 二》-說說你對CAS了解

從思想上來說,​​

​Synchronized屬于悲觀鎖​

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

​CAS屬于樂觀鎖​

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

4.CAS的缺點

  • CPU開銷較大

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

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

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

  • ABA問題

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

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

5.遺留的兩個問題:

  • Java當中CAS的底層實作
  • 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關鍵字來保證。有關volatile關鍵字的知識,我們之前有介紹過,這裡就不詳細闡述了。

接下來看一看compareAndSet方法的實作,以及方法所依賴對象的來曆:

《我要進大廠系列 二》-說說你對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當中。

《我要進大廠系列 二》-說說你對CAS了解

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

《我要進大廠系列 二》-說說你對CAS了解

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

《我要進大廠系列 二》-說說你對CAS了解

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

《我要進大廠系列 二》-說說你對CAS了解

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

《我要進大廠系列 二》-說說你對CAS了解

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

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

由于提款機硬體出了點小問題,小灰的提款操作被同時送出兩次,開啟了兩個線程,兩個線程都是擷取目前值100元,要更新成50元。

《我要進大廠系列 二》-說說你對CAS了解

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

《我要進大廠系列 二》-說說你對CAS了解

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

《我要進大廠系列 二》-說說你對CAS了解

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

《我要進大廠系列 二》-說說你對CAS了解

線程2恢複運作,由于阻塞之前已經獲得了“目前值”100,并且經過compare檢測,此時存款實際值也是100,是以成功把變量值100更新成了50。

《我要進大廠系列 二》-說說你對CAS了解
《我要進大廠系列 二》-說說你對CAS了解

這個舉例改編自《java特種兵》當中的一段例子。原本線程2應當送出失敗,正确餘額應該保持為100元,結果由于ABA問題送出成功了。

7.ABA問題解決

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

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

《我要進大廠系列 二》-說說你對CAS了解

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

《我要進大廠系列 二》-說說你對CAS了解

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

《我要進大廠系列 二》-說說你對CAS了解

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

8.小結

  1. Java語言CAS底層如何實作?

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

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

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

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

8.面試回答

CAS(Compare-and-Swap),即比較并替換,java并發包中許多Atomic的類的底層原理都是CAS。

它的功能是判斷記憶體中某個位址的值(V)是否為預期值(A),如果是就改變成新值(B),整個過程具有原子性。

具體展現于sun.misc.Unsafe類中的native方法,調用這些native方法,JVM會幫我們實作彙編指令,這些指令是CPU的原子指令,是以具有原子性。

适用場景:

1. CAS 适合簡單對象的操作,比如布爾值、整型值等;

  1. CPU開銷過大 : 在并發量比較高的情況下,如果許多線程反複嘗試更新某一個變量,卻又一直更新不成功,循環往複,會給CPU帶來很到的壓力。
  2. 不能保證代碼塊的原子性:CAS機制所保證的知識一個變量的原子性操作,而不能保證整個代碼塊的原子性。比如需要保證3個變量共同進行原子性的更新,就不得不使用synchronized或lock了。
  3. ABA問題:如果記憶體位址V初次讀取的值是A,在CAS等待期間它的值曾經被改成了B,後來又被改回為A,那CAS操作就會誤認為它從來沒有被改變過。(取錢案例)

    如何解決: 除了Compare比較記憶體中某個位址的值(V)是否為預期值(A),之外,還要添加版本号這個次元!