所謂的 ABA 問題是指在併發程式設計中,如果一個變數初次讀取的時候是 A 值,它的值被改成了 B,然後又其他執行緒把 B 值改成了 A,而另一個早期執行緒在對比值時會誤以為此值沒有發生改變,但其實已經發生變化了,這就是 ABA 問題。
比如:張三去銀行取錢,餘額有 200 元,張三取 100 元,但因為程式的問題,啟動了兩個執行緒,執行緒一和執行緒二進行比對扣款,執行緒一獲取原本有 200 元,扣除 100 元,餘額等於 100 元,此時李四給張三轉賬 100 元,於是啟動了執行緒三搶先線上程二之前執行了轉賬操作,把 100 元又變成了 200 元,而此時執行緒二對比自己事先拿到的 200 元和此時經過改動的 200 元值一樣,就進行了減法操作,把餘額又變成了 100 元。這顯然不是我們要的正確結果,我們想要的結果是餘額減少了 100 元,又增加了 100 元,餘額還是 200 元,而此時餘額變成了 100 元,顯然有悖常理,這就是著名的 ABA 的問題。
執行流程如下:
執行緒一:取款,獲取原值 200 元,與 200 元比對成功,減去 100 元,修改結果為 100 元。
執行緒二:取款,獲取原值 200 元,阻塞等待修改。
執行緒三:轉賬,獲取原值 100 元,與 100 元比對成功,加上 100 元,修改結果為 200 元。
執行緒二:取款,恢復執行,原值為 200 元,與 200 元對比成功,減去 100 元,修改結果為 100 元。
最終的結果是 100 元。
# 解決 ABA
解決 ABA 問題的一種方法是使用帶版本號的 CAS,也稱為雙重 CAS(Double CAS)或者版本號 CAS。具體來說,每次進行 CAS 操作時,不僅需要比較要修改的記憶體地址的值與期望的值是否相等,還需要比較這個記憶體地址的版本號是否與期望的版本號相等。如果相等,才進行修改操作。這樣,在修改後的值後面追加上一個版本號,即使變數的值從 A 變成了 B 再變成了 A,版本號也會發生變化,從而避免了誤判。
以下是一個使用 AtomicStampedReference 來解決 ABA 問題的示例程式碼:
import java.util.concurrent.atomic.AtomicStampedReference;public class ABADemo { private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(1, 0); public static void main(String[] args) throws InterruptedException { System.out.println("初始值:" + atomicStampedRef.getReference() + ",版本號:" + atomicStampedRef.getStamp()); // 執行緒 1 先執行一次 CAS 操作,期望值為 1,新值為 2,版本號為 0 Thread thread1 = new Thread(() -> { int stamp = atomicStampedRef.getStamp(); atomicStampedRef.compareAndSet(1, 2, stamp, stamp + 1); }); // 執行緒 2 先 sleep 1 秒,讓執行緒 1 先執行一次 CAS 操作,然後再執行一次 CAS 操作,期望值為 2,新值為 1,版本號為 1 Thread thread2 = new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } int stamp = atomicStampedRef.getStamp(); atomicStampedRef.compareAndSet(2, 1, stamp, stamp + 1); }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("最終值:" + atomicStampedRef.getReference() + ",版本號:" + atomicStampedRef.getStamp()); } }
以上程式的執行結果為:
初始值:1,版本號:0
最終值:1,版本號:2
從輸出結果可以看出,即使變數的值從 1 變成了 2 再變成了 1,使用帶版本號的 CAS 操作也能正確判斷變數是否發生了變化。