從原子性挖到CAS

無聊夫斯基發表於2018-08-26

之前在分析volatile關鍵字的時候有提到了volatile不能保證原子性,而且書上提到可以通過加鎖(synchronized和JUC中的原子類)來保證原子性。這邊炒一波冷飯,對於synchronized我也在上篇討論過它在同一時間只能允許一個執行緒去訪問一段特定的程式碼,從而保護一些變數或者資料不會被其他執行緒所修改來實現原子性。至於為什麼,上篇中也有解釋這裡就不再贅述了。對於Java JUC包中的原子類,它們的底層就是通過CAS來實現對變數的原子操作。在平常看書或者看其他大神部落格的過程中,CAS這個概念經常會被提起,既然不明白為什麼不進行了解一番呢?

CAS(compare and swap)是什麼?

(巨集觀來說)在電腦科學中,比較和交換(CAS)是多執行緒中用於實現同步的原子指令(來自Wikipedia)。(回到現實來說)CAS中包含三個引數,記憶體中的值V、期望值A和要修改的值B。 拿期望值A與記憶體中的值V進行比較,如果相同,則用B替換記憶體中的值V,否則什麼都不做。如果單執行緒的情況下,所有的操作都是一個執行緒來完成,就不需要考慮其他的問題。但是如果是多執行緒的話,同時訪問記憶體就會有不一樣的情況。

如果A、B執行緒能自覺的排隊去訪問主記憶體中的值就沒這篇文章什麼事了。 在某一時刻,執行緒A、B同時想去修改主記憶體中的共享變數。根據CAS原則有3個步驟要走,讀值比較替換。它們同時讀到了記憶體中的值V,並將值以期望值A的形式存到自己的工作記憶體(根據Java記憶體模型,每個執行緒有自己的工作記憶體來對變數進行操作)中。接著就是執行緒進行寫值操作的時候了,記憶體中的值只有一個,若想操作它總要有個先來後到。若A執行緒眼疾手快搶佔先機(假設這時並沒有第三個執行緒改變記憶體中的值V),用期望值A與記憶體中的值V比較,發現一致然後將自己的更新值B替換主記憶體的值V。然後執行緒B來了,在做比較的時候發現自己的期望值A與記憶體中的值V不相等,只能悻悻而歸。

CAS的優點?

上面說到,volatile關鍵字不能保證原子性,開發過程中可以通過加鎖的形式來保證。這裡要鞭屍一波synchronized關鍵字,多執行緒的情況下,它能保證原子性。但是當一個執行緒獲得鎖後,其他執行緒就被掛起,當獲得鎖的執行緒釋放鎖後其他執行緒才能重新去競爭鎖。每一次執行緒的阻塞和喚醒都需要作業系統的介入,需要在使用者態和和核心態之間切換,而這種切換會消耗大量的系統資源。而使用CAS基於指令來實現,不需要進入核心或者切換執行緒,所以效能上會比synchronized關鍵字要好。

CAS的缺陷?

上面講了它的有點,現在嘴臭一波。

  1. CPU開銷大
    在併發量大的情況下,CAS自旋的概率會變大。若多個執行緒反覆的去嘗試更新一個變數卻一直不成功,會一直迴圈等待重試,直到耗盡CPU分配給該執行緒的時間片,對CPU造成巨大的壓力。
  2. 不能保證程式碼塊的一致性
    CAS機制只能保證一個變數的原子性操作,多個變數時還是隻能使用synchronized關鍵字。
  3. ABA問題
    線上程讀取變數和替換變數值的過程中存在一定的時間差,在這個時間差中記憶體中的變數值可能從A變成B再變成A,當前執行緒無法判斷當前V值是否發生變化。對於如何解決這個ABA的問題,《併發程式設計藝術》中給出為每一個變數新增標識,一旦對變數的值修改後,對標識也進行操作。在每次CAS比較的過程中,同時去比較標識的值來判斷當前的V值是否發生變化。Java提供了AtomicStampedReference來解決ABA的問題,它通過建立Pair內部物件來維護標記的引用。原始碼部分也還好理解的, 當前的引用和標識與預期的引用和標識相等,並且更新後的引用和標誌與當前的引用和標誌相等則直接返回true,否則通過生成一個新的Pair物件與當前Pair進行CAS替換。
public class AtomicStampedReference<V> {

    private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }
    
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }
}
複製程式碼
CAS具體的使用場景?

這裡也就拿上面說到的JUC下原子操作類來舉例子。原子操作類是在java.util.concurrent.atomic包下一系列以Atomic開頭的包裝類。AtomicInteger也可以保證共享變數在多執行緒環境下是執行緒安全的。

AtomicInteger原始碼

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    // value成員屬性的記憶體地址相對於物件記憶體地址的偏移量
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
    public final int get() {
        return value;
    }
    
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
}
複製程式碼

在除錯AtomicInteger原始碼時發現,不同的AtomicInteger的物件中valueOffset的值都是相同的,由此產生疑問,最後查閱資料發現valueOffSet為value成員屬性的記憶體地址相對於物件記憶體地址的偏移量。

Unsafe是CAS的核心類,Java方法無法直接訪問底層的系統,需要通過本地方法(native)來訪問。Unsafe可以直接操作特定的記憶體資料。value值用volatile關鍵字修飾,保證了變數在多執行緒操作中的記憶體可見性。

馬後炮

對於執行緒、鎖這邊的知識點很多都是互相關聯的,很難對這邊這麼多的概念進行一個系統的羅列。最近寫的幾篇都是偏理論的而且大部分還是書上的內容,寫的還是比較虛,不過每次在推敲著寫的時候會發現很多不起眼的其他相關知識,如果感覺有用還是會貼到文章的句子中,還是希望繼續寫下去的時候能多有一些自己的想法。

最後還是那句話,學習的最終目的並不是為了面試,面試只是一個激勵學習的動機。把握面試題,享受學習新知識的樂趣。

參考:

《併發程式設計的藝術》

相關文章