🔁 解決 Java 中的 ABA 問題 — 使用 AtomicStampedReference
❓ 什麼是 ABA 問題?
ABA 問題發生在使用 CAS(Compare-And-Swap)時:
- 執行緒 A 讀到某個變數值為 A
- 執行緒 B 將該變數從 A ➝ B ➝ A
- 執行緒 A 再次使用 CAS,認為值沒變,繼續執行,但其實值已變動過!
這會導致程式誤以為狀態未變,但實際上已經被其他執行緒修改過。
🧰 解法:使用 AtomicStampedReference
Java 提供 AtomicStampedReference<T>
,這是一種「附加版本號(stamp)」的原子參考,可以避免 ABA 問題。
它在每次變更時,都會同步更新一個 stamp 值,用來追蹤版本。
🧪 範例程式碼
1 | import java.util.concurrent.atomic.AtomicStampedReference; |
🔍 程式碼步驟說明
1 | String initialRef = "A"; |
- 初始化一個變數,值是
"A"
,版本號(stamp)是0
。
🧵 Thread A 讀值與 stamp:
1 | int[] stampHolder = new int[1]; |
- 模擬「執行緒 A」讀取目前的值和 stamp(版本號),值是
"A"
,stamp 是0
。
📌 Java 的參數傳遞是「值的複製」(如果上述方式看不懂可以點開)
Java 方法呼叫時,參數是傳值。
也就是說,如果直接傳 int stamp
進去,方法內就算修改 stamp,你外面也是看不到的。
✅ 解法:利用陣列來模擬「傳參考」
在 Java 裡,物件與陣列是以「參考」方式傳遞的。
所以我們把 int[] stampHolder = new int[1];
當作 stamp 的「容器」,讓 get()
方法可以把 stamp 寫進 stampHolder[0]
。
這樣你就能在方法外部拿到最新的版本號(stamp)。
🔍 實際操作流程
1 | AtomicStampedReference<String> atomicRef = new AtomicStampedReference<>("A", 0); |
🧠 延伸:AtomicMarkableReference 也採用類似技巧
這種單元素陣列通常用於在不同的方法或類別之間共享一個可變的值,特別是需要通過方法參數傳遞一個可修改的 boolean 值時。因為 Java 中的基本類型是按值傳遞的,所以包裝在陣列中可以讓方法修改這個值,並且這個修改對呼叫方是可見的。
1 | AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("A", false); |
這種「雙傳出參數」技巧是 concurrent 套件中常見的 API 設計手法。
📝 小結
元素 | 說明 |
---|---|
int[] stampHolder |
stamp 的「傳出容器」 |
stampHolder[0] |
方法內部會寫入的 stamp 結果值 |
Java 陣列傳參 | 允許內部修改陣列內容,模擬傳參考效果 |
我是分隔線(📌 Java 的參數傳遞說明結束)
🧵 其他執行緒偷偷修改值:
1 | atomicRef.compareAndSet("A", "B", stamp, stamp + 1); |
- 模擬另一個執行緒:
- 把
"A"
改成"B"
,stamp 從0
→1
。 - 再把
"B"
改回"A"
,stamp 從1
→2
。
- 把
雖然值又變回 “A”,但 stamp 已經變成 2,不是執行緒 A 最初讀到的 0。
🔧 真實開發中會怎麼做?(如果上述方式看不懂可以點開)
在實務上,如果在多執行緒環境中使用 AtomicStampedReference
,每一次操作 stamp 都應該是根據最新值去 +1,而不是硬寫 +1, +2
。
也就是說,會這樣寫 👇:
1 | int[] stampHolder = new int[1]; |
這樣 stamp 是根據目前取得的最新值來進行版本推進的,而不是硬寫「上次是 +1」這種寫法。
🧪 那為什麼教學範例會用硬編 stamp?
因為教學想要簡單快速模擬 ABA 的流程,像這樣:
- A 執行緒讀取值 “A”,stamp 是 0
- 模擬別人先改成 B,stamp +1
- 再改回 A,stamp +2
- A 執行緒拿著一開始的 stamp 嘗試更新,發現 stamp 已變,CAS 失敗
如果用「動態 stamp」,反而不容易呈現「ABA 問題」。
✅ 實務中建議的 stamp 操作流程
範例改寫如下(更像真實開發):
1 | // 讀取當前值與 stamp |
這樣才是 thread-safe 的操作方式,不用自己手動加 +1、+2 記數字。
我是分隔線(🔍 程式碼步驟說明結束)
🧵 Thread A 嘗試 CAS:
1 | boolean result = atomicRef.compareAndSet("A", "C", stamp, stamp + 1); |
- 執行緒 A 嘗試用原來的 stamp(0)來把
"A"
換成"C"
,結果失敗!
因為 stamp 已經不是 0,而是 2!
✅ 輸出結果:
1 | System.out.println("Thread A: CAS 結果 = " + result); // false |
這證明:
- 雖然值沒變,看起來還是
"A"
。 - 但透過 stamp(版本號),我們可以知道這個值曾經被改過!
✅ 結論
問題 | 解法 |
---|---|
ABA 問題:只看值會以為沒變 | AtomicStampedReference 同時比較值與 stamp,能發現值曾被修改過 |
適用場景 | 寫 lock-free 資料結構、或需要高並發下確保一致性時 |
✅ 優點
- 避免 ABA 問題:版本號 stamp 會變動,即使值重複也能分辨是否已被修改
- 較適合處理複雜狀況下的原子參考比對
📚 類似類別
AtomicMarkableReference<T>
:與Stamped
類似,但記錄的是一個 boolean 狀態(是否已標記)
🧠 小結
技術 | 說明 |
---|---|
AtomicReference<T> |
一般 CAS,無法避免 ABA 問題 |
AtomicStampedReference<T> |
搭配 stamp(版本號)來避免 ABA |
AtomicMarkableReference<T> |
用 boolean 記錄是否變更 |
建議在會重複變更同樣值的場景下使用 AtomicStampedReference,以確保資料一致性與執行緒安全。