作者: 雅各布·詹科夫
原文: http://tutorials.jenkov.com/j...
翻譯: 潘深練 如您有更好的翻譯版本,歡迎 ❤️ 提交 issue 或投稿哦~
更新: 2022-02-24
CAS
(compare and swap) 是併發演算法設計時使用的一種技術。基本上,CAS
是將變數的值與期望值進行比較,如果值相等,則將變數的值交換設定為新值。CAS
可能聽起來有點複雜,但一旦你理解它實際上相當簡單,所以讓我進一步詳細說明這個主題。
順便說一句,compare and swap 有時是 CAS
的縮寫,所以如果你看到一些關於併發的文章或視訊提到 CAS
,它很有可能是指 compare and swap(比較並交換)操作。
CAS教程視訊
如果您喜歡視訊,我在這裡有這個CAS
的視訊教程版本:(綠色上網)
CAS視訊教程
CAS的使用場景(Check Then Act)
併發演算法中常見的模式是先檢查後執行(check then act
)模式。當程式碼首先檢查變數的值然後根據該值進行操作時,就會出現先檢查後執行(check then act
)模式。這是一個簡單的例子:
public class ProblematicLock {
private volatile boolean locked = false;
public void lock() {
while(this.locked) {
// 忙等待 - 直到 this.locked == false
}
this.locked = true;
}
public void unlock() {
this.locked = false;
}
}
此程式碼不是多執行緒鎖的 100%
正確實現。這就是我給它命名的原因 ProblematicLock
(問題鎖) 。然而,我建立了這個錯誤的實現來說明如何通過CAS
功能來解決它的問題。
該lock()
方法首先檢查成員變數是否locked
等於false
。這是在while-loop
內部完成的。如果locked
變數是false
,則該lock()
方法離開while
迴圈並設定locked
為true
。換句話說,該 lock()
方法首先檢查變數的值locked
,然後根據該檢查進行操作。先檢查,再執行。
如果多個執行緒幾乎同時刻訪問同一個 ProblematicLock
例項,那以上的 lock()
方法將會有一些問題,例如:
如果執行緒 A 檢查locked
的值為 false
(預期值),它將退出 while-loop
迴圈執行後續的邏輯。如果此時有個執行緒B線上程A將locked
值設定為 true
之前也檢查了 locked
的值,那麼執行緒B也將退出 while-loop
迴圈執行後續的邏輯。這是一個典型的資源競爭問題。
先檢查後執行(Check Then Act)必須是原子性的
為了在多執行緒應用程式中正常工作(以避免資源競爭),先檢查後執行(Check Then Act
)必須是原子性的。原子性的意思是檢查和執行動作都作為原子(不可分割的)程式碼塊執行。任何開始執行該塊的執行緒都將完成該塊的執行,而不受其他執行緒的干擾。不允許其他執行緒在同一時刻執行相同原子塊。
使Java
程式碼塊具有原子性的一種簡單方法是使用Java
的synchronized
關鍵字對其進行標記。可以參閱 關於synchronized 的內容。這是ProblematicLock
之前使用synchronized
關鍵字將lock()
方法轉換為原子程式碼塊的方法:
public class MyLock {
private volatile boolean locked = false;
public synchronized void lock() {
while(this.locked) {
// 忙等待 - 直到 this.locked == false
}
this.locked = true;
}
public void unlock() {
this.locked = false;
}
}
現在方法lock()
已申明同步,因此同一例項的lock()
方法在同一時刻只允許被一個執行緒訪問執行。相當於 lock()
方法是原子性的。
阻塞執行緒的代價很大
當兩個執行緒試圖同時進入Java
中的一個同步塊時,其中一個執行緒將被阻塞,而另一個執行緒將被允許進入同步塊。當進入同步塊的執行緒再次退出該塊時,等待中的執行緒才會被允許進入該塊。
如果執行緒被允許訪問執行,那麼進入一段同步程式碼塊的代價並不大。但是如果因為已有一個執行緒在同步塊中執行導致另一個執行緒被迫等阻塞,那麼這個阻塞執行緒的代價就很大。
此外,當同步塊再次空閒時,您無法準確地確定何時能解除阻塞的執行緒。這通常取決於作業系統
或執行平臺
來 協調 阻塞執行緒的 阻塞解除。當然,在阻塞執行緒被解除阻塞並允許進入之前不會花費幾秒鐘或幾分鐘,但是可能會浪費一些時間用於阻塞執行緒,因為它本來可以訪問共享資料結構的。這在此處進行了說明:
硬體提供的原子性CAS操作
現代 CPU
內建了對CAS
的原子性操作的支援。在某些情況下,可以使用CAS
操作來替代同步塊或其他阻塞資料結構。CPU
保證一次只有一個執行緒可以執行CAS
操作,即使跨 CPU
核心也是如此。稍後在程式碼中有示例。
當使用硬體或 CPU
提供的CAS
功能而不是作業系統或執行平臺提供的 synchronized
、lock
、mutex
(互斥鎖) 等時,作業系統或執行平臺不需要處理執行緒的阻塞和解除阻塞。這使得使用CAS
的執行緒等待執行操作的時間更短,並且擁有更少的擁塞和更高的吞吐量。如下圖所示:
如您所見,試圖進入共享資料結構的執行緒永遠不會被完全阻塞。它不斷嘗試執行CAS
操作,直到成功,並被允許訪問共享資料結構。這樣執行緒可以進入共享資料結構之前的延遲被最小化。
當然,如果執行緒在重複執行CAS
的過程中等待很長時間,可能會浪費大量的CPU
週期,而這些CPU
週期本來可以用在其他任務(其他執行緒)上。但在許多情況下,情況並非如此。這取決於共享資料結構被另一個執行緒使用多長時間。實際上,共享資料結構的使用時間不長,因此上述情況不應該經常發生。但同樣這取決於具體情況、程式碼、資料結構、嘗試訪問資料結構的執行緒數、系統負載等。相反,阻塞的執行緒根本不使用CPU
。
Java中的CAS
從 Java 5
開始,您可以通過java.util.concurrent.atomic
包中的一些新的原子類訪問 CPU
級別的CAS
方法。這些類有:
- AtomicBoolean
- AtomicInteger
- AtomicLong
- AtomicReference
- AtomicStampedReference
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
使用 Java 5+
附帶的 CAS
功能而不是自己實現的優勢在於,Java 5+
中內建的 CAS
功能允許您的應用程式利用 CPU
的底層能力執行CAS
操作。這使您的CAS
實現程式碼更快。
CAS的保障性
CAS
功能可用於保護臨界區(Critical Section
),從而防止多個執行緒同時執行臨界區。
?> critical section 是每個執行緒中訪問臨界資源的那段程式碼,不論是硬體臨界資源,還是軟體臨界資源,多個執行緒必須互斥地對它進行訪問。每個執行緒中訪問臨界資源的那段程式碼稱為臨界區(Critical Section
)。每個執行緒中訪問臨界資源的那段程式稱為臨界區(Critical Section
)(臨界資源是一次僅允許一個執行緒使用的共享資源)。每次只准許一個執行緒進入臨界區,進入後不允許其他執行緒進入。
下面的一個示例,展示瞭如何使用AtomicBoolean
類的CAS
功能來實現前面顯示的lock()
方法並因此起到保障作用(一次只有一個執行緒可以退出該lock()
方法)。
public class CompareAndSwapLock {
private AtomicBoolean locked = new AtomicBoolean(false);
public void unlock() {
this.locked.set(false);
}
public void lock() {
while(!this.locked.compareAndSet(false, true)) {
// busy wait - until compareAndSet() succeeds
}
}
}
注意這個locked
變數不再是一個布林型別而是AtomicBoolean
型別,此類有一個compareAndSet()
方法,會把例項的值(變數locked)與第一個引數(false
)進行比較,如果比較結果相同(即locked的值等於第一個引數false),那麼會將例項的值 locked
與期望值true
交換(即把locked變數設定為true,表示鎖住了)。如果交換成功則compareAndSet()
方法會返回 true
,如果沒有交換成功則返回 false
。
在上面的例子中,compareAndSet()
方法呼叫比較了locked
變數值與false
值,如果locked
變數值的結果值就是false
,那麼就是設定locked
值為true
。
由於一次只能允許一個執行緒執行該compareAndSet()
方法,因此只有一個執行緒能夠看到AtomicBoolean例項值為 false
,從而將其交換為true
。因此,每次只有一個執行緒能夠退出while-loop
(while迴圈),通過呼叫 unlock()
方法設定 locked
為 false
使得每次只有一個執行緒的 CompareAndSwapLock
是解鎖狀態的。
CAS實現樂觀鎖
也可以使用CAS
功能作為樂觀鎖機制。樂觀鎖機制允許多個執行緒同時進入臨界區,但只允許其中一個執行緒在臨界區結束時提交其工作。
下面是一個使用樂觀鎖策略的併發計數器類示例:
public class OptimisticLockCounter{
private AtomicLong count = new AtomicLong();
public void inc() {
boolean incSuccessful = false;
while(!incSuccessful) {
long value = this.count.get();
long newValue = value + 1;
incSuccessful = this.count.compareAndSet(value, newValue);
}
}
public long getCount() {
return this.count.get();
}
}
請注意 inc()
方法是如何從 AtomicLong
例項變數count
中獲取現有計數值的。然後根據舊值計算出新值。最後,inc()
方法嘗試通過呼叫AtomicLong
例項的compareAndSet()
方法來設定新值。
如果AtomicLong
例項值count
在比較時仍然擁有與上次獲取時(long value = this.count.get()
)的值相同,那麼compareAndSet()
會執行成功。但是假如有另一個執行緒在同一時刻已經呼叫增加了AtomicLong
例項值(指有一個執行緒在之前已經呼叫成功compareAndSet()
方法了,一般認為是資源競爭),則compareAndSet()
呼叫將失敗,因為預期值value
不再等於儲存在中的值AtomicLong
(原值已經被前一個執行緒更改過)。在這種情況下,inc()
方法將在 while-loop
(while迴圈)中進行另外一次迭代並嘗試再次增加AtomicLong
值。
(本篇完)
原文: http://tutorials.jenkov.com/j...
翻譯: 潘深練 如您有更好的翻譯版本,歡迎 ❤️ 提交 issue 或投稿哦~