併發編程之CAS的原理

一.什麼是CAS?

CAS(compare And Swap),中文叫比較交換,是一種無鎖原子算法。
過程是這樣:

它包含3個參數CAS (V,E,N),V 表示要更新變量的值,E表示預期值,,N表示新值。僅當V值等於E值時,纔會將V的值設爲N,如果V值和E值不同,則說明已經有其它線程做了更新,則當前線程則什麼都不做,最後,CAS返回當前V的真實值。CAS操作時抱着樂觀的態度進行的,它總是認爲自己可以成功完成操作。

image.png

當多個線程同時使用CAS操作一個變量時,只有一個會勝出,併成功更新,其餘線程均會失敗。失敗的線程不會掛起,只是被告知失敗,並且允許再次嘗試,當然也允許實現的線程放棄操作。基於這樣的原理,CAS操作即使沒有鎖,也可以發現其它線程對當前線程的干擾。


與鎖相比,使用CAS會使程序看起來更復雜一些,但由於其非阻塞的,它對死鎖問題天生免疫,並且,線程間的相互影響也非常小。更爲重要的是,使用無鎖的方式完全沒有鎖競爭帶來的系統開銷,也沒有線程間頻繁調度帶來的開銷,因此,它要比基於鎖的方式擁有的更優越的性能。


簡單的說,CAS需要你額外給出一個期望值,也就是你認爲這個變量現在應該是什麼樣子的,如果變量不是你想想象的那樣,那就說明被別的線程修改過了。你就需要重新讀取,再次嘗試修改就好了。

二.CAS底層原理

這樣歸功於硬件指令集的發展,實際上,我們可以使用同步將這兩個操作變成原子的,但是這麼做就沒有意義了。所以我們只能靠硬件來完成,硬件保證一個從語義上看起來需要多次操作的行爲只通過一條處理器指令就能完成。這類指令常用的有:

  1. 測試並設置(Tetst-and-Set)
  2. 獲取並增加(Fetch-and-Increment)
  3. 交換(Swap)
  4. 比較並交換(Compare-and-Swap)
  5. 加載鏈接/條件存儲(Load-Linked/Store-Conditional)

CPU實現指令有2種方式:

  • 1.通過總線鎖定來保證原子性

總線鎖定其實就是處理器使用了總線鎖,所謂總線鎖就是使用處理器提供的一個 LOCK# 信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那麼該處理器可以獨佔共享內存。但是該方法成本太大。因此有了下面的方式。

  • 2.通過緩存鎖定來保證原子性

所謂 緩存鎖定 是指內存區域如果被緩存在處理器的緩存行中,並且在Lock 操作期間被鎖定,那麼當他執行鎖操作寫回到內存時,處理器不在總線上聲言 LOCK# 信號,而時修改內部的內存地址,並允許他的緩存一致性機制來保證操作的原子性,因爲緩存一致性機制會阻止同時修改兩個以上處理器緩存的內存區域數據(這裏和 volatile 的可見性原理相同),當其他處理器回寫已被鎖定的緩存行的數據時,會使緩存行無效。

注意:有兩種情況下處理器不會使用緩存鎖定。

  1. 當操作的數據不能被緩存在處理器內部,或操作的數據跨多個緩存行時,則處理器會調用總線鎖定。
  2. 有些處理器不支持緩存鎖定,對於 Intel 486 和 Pentium 處理器,就是鎖定的內存區域在處理器的緩存行也會調用總線鎖定。

三.舉例說明

public class TestCAS {

    /**
     * 定義原子類型Integer  count
     */
    private static AtomicInteger atomicCount = new AtomicInteger(0);

    /**
     *定義Integer count
     */
    private static Integer count = 0;


    /**
     * 進行 count++
     */
    public static void countIncrement(){
        count ++;
    }

    /**
     * 進行 atomicCount++
     */
    public static void atomicCountIncrement(){
        atomicCount.getAndIncrement();
    }

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 2000; i++) {
            new Thread(() ->{
                TestCAS.countIncrement();
                TestCAS.atomicCountIncrement();
            }).start();
        }

        TimeUnit.SECONDS.sleep(2);
        System.out.println("atomicCount="+atomicCount.get());
        System.out.println("count="+count);
    }

}

結果是:image.png

四.源碼分析

JUC下的atomic類都是通過CAS來實現的,下面就以AtomicInteger爲例來說明CAS的實現,如圖:
image.png
image.png
image.png
image.png


Unsafe是CAS的核心類,Java無法直接訪問訪問底層操作系統,而是本地方法(native)方法來訪問。不過儘管如此,JVM還是開了一個後門:Unsafe,它提供了硬件級別的原子操作。


image.png


atomicCount.getAndIncrement() 內部調用了unsafe的getAndAddInt方法,在getAndAddInt() 方法中主要是看compareAndSwapInt方法
image.png

CAS可以保證一次的讀-改-寫操作是原子操作,在單處理器上該操作容易實現,但是在多處理器上實現就有點兒複雜了。

緩存加鎖:
其實針對於上面那種情況我們只需要保證在同一時刻對某個內存地址的操作是原子性的即可。緩存加鎖就是緩存在內存區域的數據如果在加鎖期間,當它執行鎖操作寫回內存時,處理器不在輸出LOCK#信號,而是修改內部的內存地址,利用緩存一致性協議來保證原子性。緩存一致性機制可以保證同一個內存區域的數據僅能被一個處理器修改,也就是說當CPU1修改緩存行中的i時使用緩存鎖定,那麼CPU2就不能同時緩存了i的緩存行


CAS的缺點:
CAS雖然高效的解決了原子操作,但是還是存在一些缺陷的。


主要表現爲三個方面:
1.循環時間太長
2.只能保證一個共享變量的原子操作
3.ABA問題

循環時間長:

如果CAS一直不成功呢?這種情況絕對有可能發生,如果自旋CAS長時間地不成功,則會給CPU帶來非常大的開銷。在 JUC中有些地方就限制了CAS自旋的次數,例如BlockingQueue的SynchronousQueue。

只能保證一個共享變量的原子操作:

看了CAS的實現就知道這隻能針對一個共享變量,如果是多個共享變量就只能使用鎖了,當然如果你有辦法把多個變量整成一個變量,利用CAS也不錯。例如讀寫鎖中state的高地位

ABA問題:

CAS需要檢查操作值有沒有發生改變,如果沒有發生改變則更新。但是存在這樣一種情況:如果一個值原來是A,變成了B,然後又變成了A,那麼在CAS檢查的時候會發現沒有改變,但是實質上它已經發生了改變,這就是所謂的ABA問題。對於ABA問題其解決方案是加上版本號,即在每個變量都加上一個版本號,每次改變時加1,即A —> B —> A,變成1A —> 2B —> 3A。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章