✅ CAS 与 ABA 问题及版本号机制详解笔记
一、CAS 是什么?
CAS(Compare-And-Swap) 是一种无锁并发原子操作,用于多线程环境下对共享变量进行安全更新。
方法定义(以 AtomicInteger
为例):
boolean compareAndSet(expectedValue, newValue)
-
如果当前值 ==
expectedValue
,则将其更新为newValue
-
否则更新失败,说明值已经被其他线程修改
二、什么是 ABA 问题?
📌 问题定义:
CAS 在比较当前值和期望值时,只能判断值是否相等,而无法判断值在期间是否被其他线程改过然后又改回来。
🧠 举个经典示例:
初始值 A = 100
线程1 读取到 A = 100,准备 CAS 修改为 200
线程2 把 A 改为 150,然后又改回 100
线程1 执行 compareAndSet(100, 200) → 成功
❗问题:虽然值看起来没变,但实际经历了变化 → 潜在的不一致
这就叫做 ABA 问题,即:
A → B → A,但 CAS 只看到“还是 A”
三、为什么 CAS 无法避免 ABA?
-
CAS 只比较“当前值”和“预期值”是否一致
-
无法知道“值是否在期间变过”
CAS 本质是基于值的等价判断,不具备“变动历史”的感知能力
四、解决 ABA 问题的方式:引入版本号机制
✅ 正确方式:值 + 版本号 分开存储
-
给每个值增加一个版本号(stamp),每次成功更新都自增版本号
-
CAS 时不仅比较值,还比较版本号
五、带版本号的原子类:AtomicStampedReference<V>
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 1);
使用示例:
int[] stampHolder = new int[1];
int value = ref.get(stampHolder); // 获取值和版本号
boolean success = ref.compareAndSet(
100, // 期望值
200, // 新值
stampHolder[0], // 当前版本号
stampHolder[0] + 1 // 新版本号
);
✅ 只有值和版本号都匹配,CAS 才成功,从而彻底解决 ABA 问题。
六、能不能把值 A 直接当成时间戳或版本号用?
❌ 不能。值 A 是业务数据,不能直接拿来当版本号
✅ 解决方案是 “值 + 版本号” 分开存储 → 不会破坏原有业务数据
七、扩展:常见变种类对比
类名 | 是否解决 ABA | 特点 |
---|---|---|
AtomicInteger / AtomicReference | ❌ | 只有值,无版本 |
AtomicStampedReference | ✅ | 值 + 整型版本号 |
AtomicMarkableReference | ✅ | 值 + 标志位(boolean),判断是否被修改过 |
自增值方案(值单调递增) | ✅(间接) | 值本身不会回退,所以不会出现 ABA,但场景有限 |
八、结论总结
-
ABA 本质问题:CAS 看不出值“曾经变过”
-
根本解决方法:加入版本号或标志位,感知中间变化
-
实践推荐:使用
AtomicStampedReference
或AtomicMarkableReference
,在高并发场景(如锁、队列)中避免 ABA 问题