ABA問題的本質及其解決辦法

簡介

CAS的全稱是compare and swap,它是java同步類的基礎,java.util.concurrent中的同步類基本上都是使用CAS來實現其原子性的。java

CAS的原理其實很簡單,爲了保證在多線程環境下咱們的更新是符合預期的,或者說一個線程在更新某個對象的時候,沒有其餘的線程對該對象進行修改。在線程更新某個對象(或值)以前,先保存更新前的值,而後在實際更新的時候傳入以前保存的值,進行比較,若是一致的話就進行更新,不然失敗。git

注意,CAS在java中是用native方法來實現的,利用了系統自己提供的原子性操做。程序員

那麼CAS在使用中會有什麼問題呢?通常來講CAS若是設計的不夠完美的話,可能會產生ABA問題,而ABA問題又能夠分爲兩類,咱們先看來看一類問題。github

更多精彩內容且看:算法

更多內容請訪問www.flydean.com編程

第一類問題

咱們考慮下面一種ABA的狀況:多線程

  1. 在多線程的環境中,線程a從共享的地址X中讀取到了對象A。
  2. 在線程a準備對地址X進行更新以前,線程b將地址X中的值修改成了B。
  3. 接着線程b將地址X中的值又修改回了A。
  4. 最新線程a對地址X執行CAS,發現X中存儲的仍是對象A,對象匹配,CAS成功。

上面的例子中CAS成功了,可是實際上這個CAS並非原子操做,若是咱們想要依賴CAS來實現原子操做的話可能就會出現隱藏的bug。編程語言

第一類問題的關鍵就在2和3兩步。這兩步咱們能夠看到線程b直接替換了內存地址X中的內容。post

在擁有自動GC環境的編程語言,好比說java中,2,3的狀況是不可能出現的,由於在java中,只要兩個對象的地址一致,就表示這兩個對象是相等的。區塊鏈

2,3兩步可能出現的狀況就在像C++這種,不存在自動GC環境的編程語言中。由於能夠本身控制對象的生命週期,若是咱們從一個list中刪除掉了一個對象,而後又從新分配了一個對象,並將其add back到list中去,那麼根據 MRU memory allocation算法,這個新的對象頗有可能和以前刪除對象的內存地址是同樣的。這樣就會致使ABA的問題。

第二類問題

若是咱們在擁有自動GC的編程語言中,那麼是否仍然存在CAS問題呢?

考慮下面的狀況,有一個鏈表裏面的數據是A->B->C,咱們但願執行一個CAS操做,將A替換成D,生成鏈表D->B->C。考慮下面的步驟:

  1. 線程a讀取鏈表頭部節點A。
  2. 線程b將鏈表中的B節點刪掉,鏈表變成了A->C
  3. 線程a執行CAS操做,將A替換從D。

最後咱們的到的鏈表是D->C,而不是D->B->C。

問題出在哪呢?CAS比較的節點A和最新的頭部節點是否是同一個節點,它並無關心節點A在步驟1和3之間是否內容發生變化。

咱們舉個例子:

public void useABAReference(){
        CustUser a= new CustUser();
        CustUser b= new CustUser();
        CustUser c= new CustUser();
        AtomicReference<CustUser> atomicReference= new AtomicReference<>(a);
        log.info("{}",atomicReference.compareAndSet(a,b));
        log.info("{}",atomicReference.compareAndSet(b,a));
        a.setName("change for new name");
        log.info("{}",atomicReference.compareAndSet(a,c));
    }
複製代碼

上面的例子中,咱們使用了AtomicReference的CAS方法來判斷對象是否發生變化。在CAS b和a以後,咱們將a的name進行了修改,咱們看下最後的輸出結果:

[main] INFO com.flydean.aba.ABAUsage - true
[main] INFO com.flydean.aba.ABAUsage - true
[main] INFO com.flydean.aba.ABAUsage - true
複製代碼

三個CAS的結果都是true。說明CAS確實比較的二者是否爲統一對象,對其中內容的變化並不關心。

第二類問題可能會致使某些集合類的操做並非原子性的,由於你並不能保證在CAS的過程當中,有沒有其餘的節點發送變化。

第一類問題的解決

第一類問題在存在自動GC的編程語言中是不存在的,咱們主要看下怎麼在C++之類的語言中解決這個問題。

根據官方的說法,第一類問題大概有四種解法:

  1. 使用中間節點 - 使用一些不表明任何數據的中間節點來表示某些節點是標記被刪除的。
  2. 使用自動GC。
  3. 使用hazard pointers - hazard pointers 保存了當前線程正在訪問的節點的地址,在這些hazard pointers中的節點不可以被修改和刪除。
  4. 使用read-copy update (RCU) - 在每次更新的以前,都作一份拷貝,每次更新的是拷貝出來的新結構。

第二類問題的解決

第二類問題其實算是總體集合對象的CAS問題了。一個簡單的解決辦法就是每次作CAS更新的時候再添加一個版本號。若是版本號不是預期的版本,就說明有其餘的線程更新了集合中的某些節點,此次CAS是失敗的。

咱們舉個AtomicStampedReference的例子:

public void useABAStampReference(){
        Object a= new Object();
        Object b= new Object();
        Object c= new Object();
        AtomicStampedReference<Object> atomicStampedReference= new AtomicStampedReference(a,0);
        log.info("{}",atomicStampedReference.compareAndSet(a,b,0,1));
        log.info("{}",atomicStampedReference.compareAndSet(b,a,1,2));
        log.info("{}",atomicStampedReference.compareAndSet(a,c,0,1));
    }
複製代碼

AtomicStampedReference的compareAndSet方法,多出了兩個參數,分別是expectedStamp和newStamp,兩個參數都是int型的,須要咱們手動傳入。

總結

ABA問題實際上是由兩類問題組成的,須要咱們分開來對待和解決。

本文的例子github.com/ddean2009/ learn-java-base-9-to-20

本文做者:flydean程序那些事

本文連接:www.flydean.com/aba-cas-sta…

本文來源:flydean的博客

歡迎關注個人公衆號:程序那些事,更多精彩等着您!

相關文章
相關標籤/搜索