文章目錄

  • CAS
  • 定義與實現
  • 應用
  • 原子類★
  • 自旋鎖
  • ABA問題
  • ABA問題解決方案
  • 總結


CAS

定義與實現

CAS: 全稱Compare and swap,字⾯意思:”⽐較並交換“,⼀個 CAS 涉及到以下操作:
我們假設內存中的原數據V,舊的預期值A,需要修改的新值B。

  1. ⽐較 A 與 V 是否相等。(⽐較)
  2. 如果⽐較相等,將 B 寫⼊ V。(交換)
  3. 返回操作是否成功。

偽代碼:

Java中的線程進階:CAS和原子類_num.addandget_版本號


邏輯:

函數參數:address是要操作的內存地址,expectedValue是 “預期內存裏現在的值”(對應圖裏的 “寄存器的值”),swapValue是 “要換成的值”(對應 “另一個寄存器的值”)。

操作流程:先判斷「內存地址裏的實際值」和「預期值expectedValue」是否相等 → 相等的話,就把內存地址裏的值換成swapValue,返回true;不相等就直接返回false。

原子性

圖裏強調 “CAS 是 CPU 的一條指令”—— 這意味着 “比較 + 交換” 這兩步操作是原子性的(不會被其他線程打斷)。

舉個例子:如果兩個線程同時對同一個內存地址做 CAS,CPU 會保證只有一個線程能完整完成 “比較 + 交換”,另一個線程的 CAS 會因為 “內存值已經被改了” 而失敗,這樣就避免了多線程的競態問題。

應用

原子類★

Java中的線程進階:CAS和原子類_num.addandget_原子類_02


在之前學習的過程中我們遇到過count++線程不安全問題,主要原因是自增操作不是原子的。我們通過加鎖強制將這個操作改成原子的,但是加鎖就難免效率會低一些。我們就可以通過CAS來實現count++,確保性能也能保證線程安全。只需要將count定義成內部實現了CAS的AtomicInteger即可。

其實不只是++操作,只要是涉及讀取一箇舊值,基於這個舊值處理,將新值寫回內存的操作都會被封裝成原子操作,也可以叫“讀-改-寫”操作。
簡單説:只要是 “需要先讀當前值,再基於這個值生成新值,最後寫回去” 的邏輯,原子類都會把它做成 “不可打斷的完整操作”。
比如:
“a = a + 5”(讀 a→算 a+5→寫 a)→ 原子類用addAndGet(5)封裝;
“a = max (a, 10)”(讀 a→比大小→寫 a)→ 原子類用accumulateAndGet(10, Math::max)封裝;
“a = 自定義邏輯 (a)”(讀 a→執行自定義函數→寫 a)→ 原子類用updateAndGet(自定義函數)封裝。

package Thread11_21;
import java.util.concurrent.atomic.AtomicInteger;
public class demo36 {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
//synchronized (locker){
count.getAndIncrement();
//}
}
System.out.println("t1結束");
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
//synchronized (locker){
count.getAndIncrement();
//}
}
System.out.println("t2結束");
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}

Java中的線程進階:CAS和原子類_num.addandget_封裝_03

// count++;
//count.getAndIncrement();
// ++count;
// count.incrementAndGet();
// count += n;
// count.addAndGet(n);

而此處的getAndIncrement偽代碼如下:

實際業務中,例如此處的getAndIncrement操作,用循環來確保最終的業務操作能完成。

Java中的線程進階:CAS和原子類_num.addandget_封裝_04


此處的oldValue相當於一個寄存器,存放着某一個線程操作開始時讀取到的內存的值,也就是要處理的值。如果這個值被其他的線程修改了,此時CAS的結果會是false,oldValue會重新讀取數據,重新操作一次。

自旋鎖

CAS還可以用來實現自旋鎖:

Java中的線程進階:CAS和原子類_num.addandget_原子類_05


Java中的線程進階:CAS和原子類_num.addandget_版本號_06

ABA問題

使用CAS能夠保證線程安全的原因是每次在寫入數據前,先比較“相等”。本質上是看是否有其他線程插入進來做了一些其他操作使得原數據被改變了。如果數據值沒有改變,就認為沒有線程插入進來。比如本來判定內存的值是A,再次判定還是A ,就説明沒被修改過。

但是!!!有沒有一種可能,這個A,是從剛開始的A被修改成了B,然後再修改成A?
比如説,二手的東西被翻新了,那還是新的嗎???

其實一般來説,從A又修改成了A,我再操作,似乎也沒什麼毛病。二手的東西翻新賣,只要他翻新的足夠好,讓我看不出來是二手的,也沒啥毛病。大部分場景下,這種ABA問題即使出現了,也沒啥影響。

只有一些極端的場景,ABA問題才會出bug:

Java中的線程進階:CAS和原子類_num.addandget_版本號_07


比如取錢的時候:

從一千元賬户取500元

萬一不小心手抖多按幾下取款導致出了兩個線程來扣款,一個線程修改了原來的值成為500,另一個線程比較時出錯,所以不會執行,因而最終不會有差錯。

但如果此時另一個線程加了500進去,第二個多出來的線程判斷時發現值是1000沒錯,就會執行扣500操作。

最終是1000 + 500 - 500 = 500,所以出現了線程安全問題。

還有一個更陰的場景:
線程1是加一操作,原數據為x。線程2在線程1 讀取完數據後執行了 result =x/x + x-1這樣的操作,最後數據沒變化,線程1還是加1。但是本來業務的邏輯是想原數據加了1之後再執行 result = (x+1)/x + x - 1,這樣的話也會產生偏差。

ABA問題解決方案

上述問題中,使用錢來判別中間是否有線程插入。但是錢可以增加也可以減少,所以引發了上述問題。我們只需要判別時選一個只能增加不能減少的值就可以了。

此時我們就可以引入一個概念:版本號-version

每進行一次操作,版本號都 + 1。

如下:

Java中的線程進階:CAS和原子類_num.addandget_版本號_08

但是!!!!!!!!版本號的方法只是能幫助你感知到出了問題,比如我説的第二種場景,當然對於取錢的場景是可以完美解決的。像第二種場景感知到了錯誤之後,後續還是需要由程序員來實際處理問題。

要注意,時間不可以,因為時間會涉及到“閏秒”問題:
閏秒問題本質是「天文時間(地球自轉)和原子時間(精確計時)不同步」導致的時間調整,也就是在某一個時間可能全體的操作系統都會將時間向前或者向後調一秒來覆蓋偏差。

總結

CAS 是 CPU 原生支持的原子操作,通過 “比較 - 交換” 的原子邏輯實現無鎖線程安全,核心用於封裝 “讀 - 改 - 寫” 類 RMW 操作(如原子類的自增、累加,或自旋鎖實現),兼顧性能與安全性;其核心缺陷是 ABA 問題,需通過 “值 + 版本號”(如 AtomicStampedReference)雙重校驗感知數據修改歷史,且版本號僅負責 “報警”,後續需程序員按業務場景(重試、放棄、調整邏輯)處理,實踐中簡單 RMW 操作優先用原子類,複雜場景仍需鎖機制,非關鍵場景可忽略 ABA 問題。