強大的CAS機制

程式設計師阿寶發表於2019-04-08

相信我們都知道樂觀鎖的底層是利用了CAS機制實現(如有不懂,請看上篇文章)你真的瞭解樂觀鎖、悲觀鎖嗎?

Java的CAS底層實現

強大的CAS機制

我們先來看看cnt.incrementAndGet();這個自增方法的原始碼

public final intincrementAndGet() {
        for (;;) {//失敗,迴圈重試
            int current = get();//讀取值
            int next = current + 1;//修改值
            if (compareAndSet(current, next))//比較並且賦值
                return next;
        }
    }

private volatileint value;
   public final int get(){
       return value;
}

複製程式碼

這裡需要注意一下這個get方法,為何要在宣告value時候使用volatile關鍵字呢? 那是因為volatile關鍵字保證變數的可見性!保證獲取當前值是記憶體中的最新值

而這段程式碼也是是ABA產生的原因

在以上程式碼中,可以看到compareAndSet(int expect, intupdate)的第1個引數,傳進去的並不是版本號,而是資料的舊值。也就是說,它認為,只要資料的舊值expect = 資料當前的值,則說明在此期間沒有其他執行緒修改過此資料,則把資料修改為新值update。

這種比較值,而不是比較版本號的做法,會產生經典的ABA問題。而這,也正是AtomicStampedReference要解決的。

public final boolean compareAndSet(int expect,int update) {
       return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
   private static final Unsafe unsafe = Unsafe.getUnsafe();
   private static final long valueOffset;

   static {
       try {
           //value成員變數在記憶體中的偏移量
           valueOffset = unsafe.objectFieldOffset
                   (AtomicInteger.class.getDeclaredField("value"));
       } catch (Exception ex) { throw new Error(ex); }
}
複製程式碼

接下來我們看看compareAndSet 方法,很明顯我們需要知道什麼是unsafe?compareAndSwapInt方法的具體含義,以及這幾個引數所表示的意義。

到底什麼是unsafe?java不像c/c++可以直接訪問底層作業系統,但是JVM卻留了一手,unsafe可以為我們提供硬體級別的操作。

compareAndSwapInt方法保證了Compare和Swap操作之間的原子性操作。

CAS機制中使用了3個運算元:需要讀寫的記憶體值 V,進行比較的值 A,擬寫入的新值 B,而unsafe的compareAndSwapInt方法引數包括三個元素,valueOffset引數代表了V,expect引數代表了A,update引數代表了B

ABA問題

線上程1改資料期間,執行緒2把資料改為A,再改為B,再改回到A。這個時候,執行緒1做CAS的時候,如果只是比較值,則它會認為資料在此期間沒有被改動過,而實際上資料已被執行緒2改動過3次。

我們舉一個例子

假設銀行有一個遵循CAS原理的提款機,小明有100元存款,現在需要取出50元。

由於取款機出現了點小問題,取款操作被提交了兩次,開啟了兩個執行緒,兩個執行緒都是獲取當前餘額100元,更新成50元。

理論上一個執行緒成功了,另一個執行緒失敗了,餘額只被扣了一次,餘額為50.

但是這時候,執行緒1執行成功了,執行緒2因為部分原因阻塞,這時候小明媽媽往小明賬戶中匯款50元,這時候賬戶餘額為100元,此時執行緒2恢復執行,因為阻塞之前獲取的賬戶金額為100元,此時賬戶金額也為100元,執行緒2認為一致執行成功,餘額會被更新為50元。而正確餘額應該是100元。

這就是1A-2B-3A問題

那該怎麼解決這個問題呢?其實邏輯也很簡單,我們只需要加個版本號就行了,在Compare期間不僅要比較A和V中的值,還需要比較版本號是否一致。面我們看看加入版本號的實現

//舊值,新值,舊版本號,新版本號
public boolean compareAndSet(V  expectedReference,
                                 V  newReference,
                                 int  expectedStamp,
                                 int  newStamp)
 {
       ReferenceIntegerPair<V> current = atomicRef.get();
       return  expectedReference ==current.reference &&
           expectedStamp == current.integer &&
           ((newReference == current.reference &&
              newStamp == current.integer) ||
            atomicRef.compareAndSet(current,
                                     newReferenceIntegerPair<V>(newReference,
                                                             newStamp)));
    }

   private static class ReferenceIntegerPair<T> {
       private final T reference;    //值
       private final int integer;    //版本號
       ReferenceIntegerPair(T r, int i) {
           reference = r; integer = i;
       }
    }
複製程式碼

上面的atomicRef.compareAndSet(…)的第一個引數,傳入的是一個ReferenceIntegerPair物件,它裡面包含了2個欄位:值 + 版本號。這也就意味著,它同時比較了值和版本號。

– 值不等,則肯定被其他執行緒改過了,不用再比較版本號,cas提交失敗;

值相等,再比較版本號,如果版本號也相等,則說明真的沒有被改過,cas提交成功;

值相等,版本號不等,則就是出現了ABA,CAS提交失敗。

----------------END----------------

喜歡本文的朋友,歡迎關注我的公眾號程式設計師阿寶,檢視更多精彩內容

強大的CAS機制

相關文章