20191220 CAS理論

CAS比較替換

CAS是項樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。CAS是一種非阻塞式的同步方式。

樂觀鎖是一種思想。CAS是這種思想的一種實現方式。

CAS 操作包含三個操作數 —— 內存位置(V)、預期原值(A)和新值(B)。如果內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前值。)CAS 有效地說明了“我認爲位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。”這其實和樂觀鎖的衝突檢查+數據更新的原理是一樣的。

 

在JDK1.5 中新增java.util.concurrent(J.U.C)就是建立在CAS之上的。相對於對於synchronized這種阻塞算法,CAS是非阻塞算法的一種常見實現。所以J.U.C在性能上有了很大的提升。

 

CAS會導致“ABA問題”。

CAS算法實現一個重要前提需要取出內存中某時刻的數據,而在下時刻比較並替換,那麼在這個時間差類會導致數據的變化。

比如說一個線程one從內存位置V中取出A,這時候另一個線程two也從內存中取出A,並且two進行了一些操作變成了B,然後two又將V位置的數據變成A,這時候線程one進行CAS操作發現內存中仍然是A,然後one操作成功。儘管線程one的CAS操作成功,但是不代表這個過程就是沒有問題的。

線程1準備用CAS將變量的值由A替換爲B,在此之前,線程2將變量的值由A替換爲C,又由C替換爲A,然後線程1執行CAS時發現變量的值仍然爲A,所以CAS成功。但實際上這時的現場已經和最初不同了,儘管CAS成功,但可能存在潛藏的問題。

compareAndSet( )

部分樂觀鎖的實現是通過版本號(version)的方式來解決ABA問題,樂觀鎖每次在執行數據的修改操作時,都會帶上一個版本號,一旦版本號和數據的版本號一致就可以執行修改操作並對版本號執行+1操作,否則就執行失敗。因爲每次操作的版本號都會隨之增加,所以不會出現ABA問題,因爲版本號只會增加不會減少。

 

CAS缺點

1. ABA問題。因爲CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。

從Java1.5開始JDK的atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。

Java中的線程安全問題至關重要,要想保證線程安全,就需要鎖機制。鎖機制包含兩種:樂觀鎖與悲觀鎖。悲觀鎖是獨佔鎖,阻塞鎖。樂觀鎖是非獨佔鎖,非阻塞鎖。有一種樂觀鎖的實現方式就是CAS ,這種算法在JDK 1.5中引入的java.util.concurrent中有廣泛應用。但是值得注意的是這種算法會存在ABA問題。

2.CPU開銷較大

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

 

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

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

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

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

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

    private static AtomicInteger count =new AtomicInteger(0);

    public static void main(String[] args) {
        for (int i = 0; i <5 ; 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.incrementAndGet();
                    }
                }
            }).start();
        }
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count.get());
    }

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

 

其他需要鎖的線程就掛起的情況就是悲觀鎖。

加同步鎖,讓操作變成原子操作。

但是Synchronized雖然確保了線程的安全,但是在性能上卻不是最優的,Synchronized關鍵字會讓沒有得到鎖資源的線程進入BLOCKED狀態,而後在爭奪到鎖資源後恢復爲RUNNABLE狀態,這個過程中涉及到操作系統用戶模式和內核模式的轉換,代價比較高。

BLOCKED(阻塞),當前線程正在處於等待狀態。

 

原子操作類,指的是java.util.concurrent.atomic包下。

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

而Atomic操作的底層實現正是利用的CAS機制。

 

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