CAS 演算法 —— Compare and Swap

風歌發表於2019-01-19

本文翻譯和原創各佔一半,所以還是厚顏無恥歸類到原創好了…
https://howtodoinjava.com/jav…
java 5 其中一個令人振奮的改進是新增了支援原子操作的型別,例如 AtomicInteger, AtomicLong 等。在多執行緒環境中進行簡單的自增自減操作時,這些原子類能幫助你減少很多用於多執行緒同步的複雜程式碼。這些原子類依賴於 CAS (compare and swap) 演算法,接下來我們會討論 CAS 這個概念。

樂觀鎖和悲觀鎖

傳統的鎖機制,例如 java 的 synchronized 關鍵字,他代表了 java 中悲觀鎖技術,保證了某一時刻僅有一個執行緒能訪問同步程式碼/方法。synchronized 能夠很好地工作,卻有著 (相對) 比較大的效能開銷。
樂觀鎖 (相對悲觀鎖) 對效能會有很大的幫助。他的核心思想是:你寄希望於在沒有衝突的情況下完成一次更新操作,使用樂觀鎖技術更新時會進行 “衝突檢測” 來判斷是否有其他的執行緒干擾,若是 (有其他執行緒干擾) 則視本次更新操作失敗,一般會進行重試 (可以瞭解一下CAS自旋)。Compare and Swap 就是典型的樂觀鎖技術。

CAS 演算法

CAS 演算法會先對一個記憶體變數(位置) V 和一個給定的值進行比較 A ,如果相等,則用一個新值 B 去修改這個記憶體變數(位置)。上述過程會作為一個原子操作完成 (intel處理器通過 cmpxchg 指令系列實現)。CAS 原子性保證了新值的計算是基於上一個有效值,期間如果記憶體變數(位置) V 被其他執行緒更新了,本執行緒的 CAS 更新操作將會失敗。CAS 操作必須告訴呼叫者成功與否,可以返回一個 boolean 值來表示,或者返回一個從記憶體變數讀到的值 (應該是上一次有效值)

CAS 運算元有三個:

  • 記憶體變數(位置) V,表示被更新的變數
  • 執行緒上一次讀到的舊值 A
  • 用來覆蓋 V 的新值 B

CAS 表示:“我認為現在 V 的值還是之前我讀到的舊值 A,若是則用新值 B 覆蓋記憶體變數 V,否則不做任何動作並告訴呼叫者操作失敗”。CAS 是一項樂觀鎖技術,他在更新的時候總是希望能成功 (沒有衝突),但也能檢測出來自其他執行緒的衝突和干擾

Java 中的 Compare and Swap

這裡我們關注一下ReentrantLock鎖定和解鎖那部分的原始碼

//ReentrantLock.lock()
public void lock() {
    sync.lock();
}

他依賴了其內部類Synclock(),以下是內部類 Sync (繼承了佇列同步器 AQS)

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;

    abstract void lock();
    ................

Sync還是個抽象類,一般 new ReentrantLock() 時建立的是 NonfairSync

// ReentrantLock的構造方法
public ReentrantLock() {
    sync = new NonfairSync();
}

下面就是NonfairSynclock() 方法了

final void lock() {
    if (compareAndSetState(0, 1)) // 1
        setExclusiveOwnerThread(Thread.currentThread()); // 2
    else
        acquire(1); // 3
}

  • 1 中的 compareAndSetState() 承繼自佇列同步器 AQS,封裝了 CAS 指令。因為是 NonfairSync 非公平鎖,所以一上來就嘗試搶佔鎖:給定舊值 0 並希望用新值 1 去更新記憶體變數 State。若更新成功則視為獲取鎖成功,並執行 2
  • 2 成功完成了 CAS 操作 (沒錯,當你使用 CAS 指令成功把 State 從 0 更新成 1 便視為獲取鎖,就是這麼簡單粗暴 ╮(╯▽╰)╭ ),把當前執行緒設為獨佔執行緒
  • 3 操作失敗 (被人搶先獲取鎖(╯`□′)╯╧╧),進行 acquire 操作再次嘗試獲取鎖,若還是不行,則把當前執行緒加入 AQS 等待佇列,由 AQS 來管理佇列中等待執行緒的阻塞和喚醒,具體程式碼就不貼出來了,AQS 的原始碼多處使用到 CAS 指令,有興趣的同學可以檢視

鎖用完了要釋放,下面貼出 unlock() 方法

// ReentrantLock.unlock()
public void unlock() {
    sync.release(1);
}

這裡還是依賴了 sync,release() 是 AQS 的通用方法,其內部呼叫了 tryRelease() (由 Sync 類實現),這裡直接貼出 Sync 的 tryRelease()

protected final boolean tryRelease(int releases) { // releases 引數的值是上面傳進來的 1
    int c = getState() - releases; // 1
    if (Thread.currentThread() != getExclusiveOwnerThread()) // 1.5
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) { // 2
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c); // 3
    return free;
}
  • 1 處的c 是記憶體變數 State 即將要被更新的值,因為 ReentrantLock 是可重入鎖 (當前執行緒可多次獲取鎖),所以 State 的值是可以大於 1 的。
  • 2 判斷若新值為 0,則視為鎖被釋放並設定當前獨佔執行緒為 null
  • 3 把 State 的值更新為 c,思考一下這裡的更新操作為什麼沒用到 CAS 指令?
  • 1.5 解釋了上面的疑問,只有當前獨佔執行緒有能力對 State 變數進行修改,不需要進行同步或使用 CAS

Summary

AQS 佇列同步器以及 java.util.concurrent 下各種鎖和原子類都運用到的 CAS 演算法,有時間的同學建議閱讀加深印象。

相關文章