关于CAS的ABA问题的原因以及解决?
在并发编程中,ABA 问题是使用 CAS(Compare-and-Swap,比较并交换)机制时可能出现的一种逻辑漏洞,它会导致原子操作的判断失效,进而引发潜在的程序错误。
一、什么是 ABA 问题?
1.CAS 操作的核心逻辑是:
“如果内存地址中的值等于预期值 A,就将其更新为新值 B;否则不做任何操作”。
2.ABA问题:
一个值从A被修改为B,随后又被改回A。此时,CAS 操作会认为 “值没有变化”(因为最终值仍是 A),但实际上中间已经发生过修改,可能导致依赖该值状态的逻辑出错。
二、ABA 问题的直观示例
假设两个线程(Thread1、Thread2)同时操作同一个变量
x
,初始值为A
:
- Thread1 读取
x
的值为A
,准备执行 CAS 操作(将A
改为C
),但因某种原因被阻塞。- Thread2 先执行:
- 读取
x
的值为A
,执行 CAS 将其改为B
(成功)。- 之后又读取
x
的值为B
,执行 CAS 将其改回A
(成功)。- Thread1 恢复运行,发现
x
的值仍为A
(与预期一致),于是执行 CAS 将其改为C
(成功)。
从 CAS 机制来看,Thread1 的操作成功了,但它不知道x
在中间经历了A→B→A
的变化。如果x
的状态变化会影响程序逻辑(比如链表节点的引用),就可能导致错误。
三、ABA 问题的实际危害场景
ABA 问题在涉及 “状态依赖” 的场景中危害较大,典型案例是链表操作:
假设一个单向链表初始状态为
A → B
(A 是头节点),两个线程同时执行 “删除头节点” 操作:
- Thread1 计划删除头节点 A,先读取到 “当前头节点是 A,下一个节点是 B”,但被阻塞。
- Thread2 执行:
- 删除头节点 A,链表变为
B
。- 新增一个节点 A,链表变为
A → B
(新的 A 节点)。- Thread1 恢复运行,发现 “头节点仍是 A”(与预期一致),执行 CAS 操作将头节点更新为 B。
此时,Thread1 错误地删除了新的 A 节点的下一个节点 B,导致链表结构损坏(实际应保留新 A 节点)。
四、如何解决 ABA 问题?
核心:
解决 ABA 问题的核心思路是:不仅校验值,还要校验 “版本” 或 “修改次数”,确保值的变化轨迹可追溯。
在 Java 中,主要通过以下两个原子类实现:
1. AtomicStampedReference
(版本戳记引用)
- 原理:为对象引用绑定一个整数 “版本号”(stamp),每次修改时不仅更新值,还会递增版本号。
- CAS 操作时,需同时校验值和版本号,只有两者都匹配时才更新。
// 初始化:值为"A",版本号为1
AtomicStampedReference<String> asr = new AtomicStampedReference<>("A", 1);// 获取当前值和版本号
int oldStamp = asr.getStamp(); // 1
String oldValue = asr.getReference(); // "A"// 尝试更新:仅当“当前值为A且版本号为1”时,更新为"B",版本号+1
boolean success = asr.compareAndSet(oldValue, "B", oldStamp, oldStamp + 1);
此时,即使值从 A→B→A,版本号也会从 1→2→3,Thread1 的 CAS 会因版本号不匹配而失败,避免 ABA 问题。
2. AtomicMarkableReference
(标记位引用)
- 原理:为对象引用绑定一个布尔值 “标记”(mark),而非版本号。标记仅表示 “值是否被修改过”,不记录修改次数。
- 适用于只需判断 “值是否被修改过”,无需精确版本的场景。
public static void main(String[] args) {// 初始化:值为"A",标记为false(未修改)AtomicMarkableReference<String> amr = new AtomicMarkableReference<>("A", false);boolean[] markHolder = new boolean[1];String oldValue = amr.get(markHolder); // 获取值和标记(markHolder[0]为false)// 尝试更新:仅当“当前值为A且标记为false”时,更新为"B",标记设为trueboolean success = amr.compareAndSet(oldValue, "B", false, true);System.out.println("更新成功:" + success);}