本文翻譯和原創各佔一半,所以還是厚顏無恥歸類到原創好了…
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();
}
他依賴了其內部類Sync
的 lock()
,以下是內部類 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();
}
下面就是NonfairSync
的 lock()
方法了
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 演算法,有時間的同學建議閱讀加深印象。