我們常說的 CAS 自旋鎖是什麼

小城華里發表於2019-01-19

CAS(Compare and swap),即比較並交換,也是實現我們平時所說的自旋鎖或樂觀鎖的核心操作。

它的實現很簡單,就是用一個預期的值和記憶體值進行比較,如果兩個值相等,就用預期的值替換記憶體值,並返回 true。否則,返回 false。

保證原子操作

任何技術的出現都是為了解決某些特定的問題, CAS 要解決的問題就是保證原子操作。原子操作是什麼,原子就是最小不可拆分的,原子操作就是最小不可拆分的操作,也就是說操作一旦開始,就不能被打斷,知道操作完成。在多執行緒環境下,原子操作是保證執行緒安全的重要手段。舉個例子來說,假設有兩個執行緒在工作,都想對某個值做修改,就拿自增操作來說吧,要對一個整數 i 進行自增操作,需要基本的三個步驟:

1、讀取 i 的當前值;

2、對 i 值進行加 1 操作;

3、將 i 值寫回記憶體;

假設兩個程式都讀取了 i 的當前值,假設是 0,這時候 A 執行緒對 i 加 1 了,B 執行緒也 加 1,最後 i 的是 1 ,而不是 2。這就是因為自增操作不是原子操作,分成的這三個步驟可以被干擾。如下面這個例子,10個執行緒,每個執行緒都執行 10000 次 i++ 操作,我們期望的值是 100,000,但是很遺憾,結果總是小於 100,000 的。

      
    static int i = 0;
    
    public static void add(){
        i++;
    }
    
    private static class Plus implements Runnable{

        @Override
        public void run(){
            for(int k = 0;k<10000;k++){
                add();
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException{
        Thread[] threads = new Thread[10];
        for(int i = 0;i<10;i++){
            threads[i] = new Thread(new Plus());
            threads[i].start();
        }

        for(int i = 0;i<10;i++){
            threads[i].join();
        }
        System.out.println(i);
    }

既然這樣,那怎麼辦。沒錯,也許你已經想到了,可以加鎖或者利用 synchronized 實現,例如,將 add() 方法修改為如下這樣:

public synchronized static void add(){
        i++;
    }

或者,加鎖操作,例如下面使用 ReentrantLock (可重入鎖)實現。

private static Lock lock = new ReentrantLock();
    public static void add(){
        lock.lock();
        i++;
        lock.unlock();
    }

CAS 實現自旋鎖

既然用鎖或 synchronized 關鍵字可以實現原子操作,那麼為什麼還要用 CAS 呢,因為加鎖或使用 synchronized 關鍵字帶來的效能損耗較大,而用 CAS 可以實現樂觀鎖,它實際上是直接利用了 CPU 層面的指令,所以效能很高。

上面也說了,CAS 是實現自旋鎖的基礎,CAS 利用 CPU 指令保證了操作的原子性,以達到鎖的效果,至於自旋呢,看字面意思也很明白,自己旋轉,翻譯成人話就是迴圈,一般是用一個無限迴圈實現。這樣一來,一個無限迴圈中,執行一個 CAS 操作,當操作成功,返回 true 時,迴圈結束;當返回 false 時,接著執行迴圈,繼續嘗試 CAS 操作,直到返回 true。

其實 JDK 中有好多地方用到了 CAS ,尤其是 java.util.concurrent包下,比如 CountDownLatch、Semaphore、ReentrantLock 中,再比如 java.util.concurrent.atomic 包下,相信大家都用到過 Atomic* ,比如 AtomicBoolean、AtomicInteger 等。

這裡拿 AtomicBoolean 來舉個例子,因為它足夠簡單。

public class AtomicBoolean implements java.io.Serializable {
    private static final long serialVersionUID = 4654671469794556979L;
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

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

    private volatile int value;
    
    public final boolean get() {
        return value != 0;
    }

    public final boolean compareAndSet(boolean expect, boolean update) {
        int e = expect ? 1 : 0;
        int u = update ? 1 : 0;
        return unsafe.compareAndSwapInt(this, valueOffset, e, u);
    }
   
  }

這是 AtomicBoolean 的部分程式碼,我們看到這裡面又幾個關鍵方法和屬性。

1、使用了 sun.misc.Unsafe 物件,這個類提供了一系列直接操作記憶體物件的方法,只是在 jdk 內部使用,不建議開發者使用;

2、value 表示實際值,可以看到 get 方法實際是根據 value 是否等於0來判斷布林值的,這裡的 value 定義為 volatile,因為 volatile 可以保證記憶體可見性,也就是 value 值只要發生變化,其他執行緒是馬上可以看到變化後的值的;下一篇會講一下 volatile 可見性問題,歡迎關注

3、valueOffset 是 value 值的記憶體偏移量,用 unsafe.objectFieldOffset 方法獲得,用作後面的 compareAndSet 方法;

4、compareAndSet 方法,這就是實現 CAS 的核心方法了,在使用 AtomicBoolean 的這個方法時,只需要傳遞期望值和待更新的值即可,而它裡面呼叫了 unsafe.compareAndSwapInt(this, valueOffset, e, u) 方法,它是個 native 方法,用 c++ 實現,具體的程式碼就不貼了,總之是利用了 CPU 的 cmpxchg 指令完成比較並替換,當然根據具體的系統版本不同,實現起來也有所區別,感興趣的可以自行搜一下相關文章。

使用場景

  • CAS 適合簡單物件的操作,比如布林值、整型值等;
  • CAS 適合衝突較少的情況,如果太多執行緒在同時自旋,那麼長時間迴圈會導致 CPU 開銷很大;

比如 AtomicBoolean 可以用在這樣一個場景下,系統需要根據一個布林變數的狀態屬性來判斷是否需要執行一些初始化操作,如果是多執行緒的環境下,避免多次重複執行,可以使用 AtomicBoolean 來實現,虛擬碼如下:

private final static AtomicBoolean flag = new AtomicBoolean();
    if(flag.compareAndSet(false,true)){
        init();
    }

比如 AtomicInteger 可以用在計數器中,多執行緒環境中,保證計數準確。

ABA問題

CAS 存在一個問題,就是一個值從 A 變為 B ,又從 B 變回了 A,這種情況下,CAS 會認為值沒有發生過變化,但實際上是有變化的。對此,併發包下倒是有 AtomicStampedReference 提供了根據版本號判斷的實現,可以解決一部分問題。

相關文章