深刻理解 CAS 原理 | Java

CAS(樂觀鎖)

CAS 全稱爲 Compare And Swap 翻譯過來就是比較而且交換java

  • Synchornized 是悲觀鎖,線程一旦獲得鎖,其餘的線程就只能掛起了
  • cas 的操做則是樂觀鎖,他認爲本身必定會拿到鎖,因此他會一直嘗試,直到成功拿到爲止;

CAS 機制

在看到 Compare 和 Swap 後,咱們就應該知道,CAS 裏面至少包含了兩個動做,分別是比較和交換,在如今的 CPU 中,爲這兩個動做專門提供了一個指令,就是CAH 指令,由 CPU 來保證這兩個操做必定是原子的,也就是說比較和交換這兩個操做只能是要麼所有完成,要麼所有沒有完成markdown

CAS 機制中使用了三個操做數,內存地址,舊的預期值,要修改的值;多線程

例如 a + 1 的操做,a 默認=0,app

1,在多個線程修改一個值 a 的時候,會將 a copy 一份到本身的線程內存空間中(預期值),此時預期值就是 a ,要修改的值就是 a+1 的結果,結果就是 1(要修改的值),因爲是多個線程,因此每一個線程內部都會獲得 a = 1。函數

2,接着就會執行比較而且交換, 讓線程中的預期值和主內存中的 a 進行比較,若是相同,就會提交上去,若是不相同,說明 a 的值已經被別的線程修改過了,因此就會提交失敗(這個比較和提交的操做是原子性的)。提交失敗以後,線程就會從新獲取 a 的值,而後重複這一操做。這種重複操做的方式稱爲自旋ui

栗子:this

前提:線程 A,B,主內存中的變量 count = 0;atom

  1. 線程A: 要修改 count 值,因此 copy 一份到本身的內存中,而後執行了 + 1 的操做,此時線程A中 count 預期值是 0,要修改的值爲 1spa

  2. 線程B :也修改 count 值,也執行了 + 1 的操做,此時線程 B 中 count 的預期值是 0,要修改的值爲 1,線程

  3. 線程B :開始提交到主內存了,提交的時候判斷預期值 和 主內存的 count 是同樣的,因此就會提交成功,這時主內存 count =1

  4. 線程A :也開始提交了,可是在判斷的時候發現預期值是 0,但主內存是1,不相等,因此,提交失敗,而後就會放棄本次提交。

  5. 線程A :提交失敗以後,就與從新執行步驟 1 的操做。

這種方式最終能夠理解成一種 無阻塞的多線程爭搶資源的模型。

問題

  • ABA

    仍是上面的栗子,

    在線程 A 執行 + 1,操做的時候,線程 B 已經將 + 1的結果提交的主內存了,可是這個時候他又執行了 - 1的操做提交到主內存,而且這個過程快於線程 A。

    這個時候線程 A 進行判斷和交換,發現修改的值和主內存的值相同,而後將計算的結果提交了。


    在線程 A 執行的過程當中,線程 B 修改了值,而且將值又修改了回去,雖說結果並無變化,可是值已經被操做過了

    這就是典型的 ABA 問題

    那麼如何解決呢?


    其實解決起來很是簡單,只須要增長一個版本戳便可,在更新值的時候判斷一下版本戳便可。

    在 Java 中也有使用版本戳的實現,就是 AtomicMarkableReferenceAtomicStampedReference

    AtomicMarkReference :只關心這個變量有沒有被動過

    AtomicStampedReferrence :不但關心這個變量有麼有動過,而且關心這個變量被動了幾回,例如

    val asr = AtomicStampedReference<String>("345", 0)
    
    fun main() {
        val oldStamp = asr.stamp
        val oldReference = asr.reference
        println("$oldReference ---- $oldStamp")
    
        val t1 = Thread {
            println("${Thread.currentThread().name} 當前變量值:${asr.reference} 當前版本:${asr.stamp}" +
                    " ${asr.compareAndSet(asr.reference, "3456", asr.stamp, asr.stamp + 1)}")
        }
        val t2 = Thread {
            println("${Thread.currentThread().name} 當前變量值:${asr.reference} 當前版本:${asr.stamp}" +
                    " ${asr.compareAndSet(asr.reference, "34567", asr.stamp, asr.stamp + 1)}")
        }
        t1.start()
        t1.join()
        t2.start()
        t2.join()
    
        println("${asr.reference} ---- ${asr.stamp}")
    }
    345 ----  0
    Thread-0   當前變量值:345  當前版本:0   true
    Thread-1   當前變量值:3456  當前版本:1   true
    34567 ----  2
    複製代碼

    在修改字符串的時候,要傳入已經修改過的字符串和版本號,負責就會修改錯誤

  • 開銷問題

    在 CAS 期間,線程是不會休息的,線程若是長時間沒法提交,內部就一直在進行自旋,這樣就會產生比較大的內存開銷

  • CAS 只可以保證一個共享變量的原子操做

    CAS 只能保證對一個內存地址進行原子操做,因此說使用範圍會有必定限制

    例如:若是在執行 a+1 的下面加上,b+1,c +1,這種狀況就會出現問題,這種時候反而使用 Syn 比較方便


    其實 Java 中也提供了能夠修改多個變量的原子操做

    AtomicReference:將須要修改的包裝成一個對象,而後使用 AtomicReference 的 compareAndSet 方法進行替換便可

    fun main() {
        val user = User("張三", 31)
        val atomicReference = AtomicReference<User>(user)
        println("${atomicReference.get().name} --- ${atomicReference.get().age}")
    
        atomicReference.compareAndSet(user, User("李四", 20))
        println("${atomicReference.get().name} --- ${atomicReference.get().age}")
    }
    
    class User(val name: String, val age: Int)
    複製代碼

    上面的代碼就保證了修改多個變量,實際上就是更新對象

實例

在 java 的 atomic 包下,一系列以 Atomic 開頭的包裝類,如 AtomicBoolean AtomicInteger 等,分別用於 int,bool,long 等類型的原子操做,其原理都是用的 cas

核心實現以下:

//使用將給定函數應用於當前值和給定值的結果,以原子方式更新當前值,並返回更新後的值。 該函數應無反作用,由於當嘗試更新因爲線程間爭用而失敗時,可能會從新應用該函數。 應用該函數時,將當前值做爲其第一個參數,並將給定的update做爲第二個參數 
public final int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction) {
    //預期值和要更新的值
        int prev, next;
        do {
            //獲取到預期值,也就是當前值
            prev = get();
            //計算要更新的值
            next = accumulatorFunction.applyAsInt(prev, x);
            //更新成功則退出循環,不然從新計算
        } while (!compareAndSet(prev, next));
        return next;
}

 //注意:這個比較而且 set 的操做是原子性的
 //參數:指望–指望值 更新–新價值
 //返回值:
 //若是成功,則爲true 。 錯誤返回表示實際值不等於指望值
public final boolean compareAndSet(int expect, int update) {
     return U.compareAndSwapInt(this, VALUE, expect, update);
}
複製代碼

若是本文有幫助到你的地方,不勝榮幸,若有文章中有錯誤和疑問,歡迎你們提出!

相關文章
相關標籤/搜索