如何解决 CAS 的 ABA 问题:从版本号机制到 AtomicStampedReference 深度解析
文章目录
问题
在多线程高并发场景下,CAS (Compare And Swap,比较并交换) 是实现无锁化编程的核心基石。它利用 CPU 的原子指令(如 cmpxchg)实现了极高性能的并发更新。然而,CAS 并非完美,其中最经典的挑战便是 ABA 问题。
本篇博客将深入探讨 ABA 问题的本质、为何不能使用“加锁”来解决,以及 Java 如何通过 AtomicStampedReference 优雅地化解这一难题。
一、 什么是 ABA 问题?
CAS 的逻辑是:“我认为值应该是 A,如果是,则更新为 B。”
ABA 现象描述:
- 线程 T1 读取共享变量值为 A。
- 线程 T2 介入,将值从 A 改为 B,随后又将其从 B 改回 A。
- 线程 T1 恢复执行,进行 CAS 操作。它发现值依然是 A,于是判定“没有变化”,更新成功。
潜在风险:
虽然数值上看起来没变,但变量的状态或引用指向的内容可能已经发生了本质变化。在某些数据结构(如无锁栈或链表)中,这可能导致指针悬挂或逻辑混乱。
二、 为什么不能通过“加锁”来解决?
面对并发冲突,最简单的直觉是加锁(synchronized 或 ReentrantLock)。但在 CAS 场景下,这与无锁的初衷背道而驰:
- 违背设计初衷:CAS 存在的意义就是为了无锁化,避免内核态切换、上下文切换和线程阻塞带来的巨大开销。
- 性能瓶颈:加锁会将并发操作变为串行,在高竞争环境下,锁的开销远大于 CAS 自旋。
- 复杂度增加:锁可能引入死锁、活锁等风险。
结论: 解决 CAS 的问题,必须坚持使用乐观锁的思想,在“无锁”的框架内寻找方案。
三、 核心方案:引入版本号
解决 ABA 的标准思想是:“不仅比较值,还要比较版本号。”
我们为变量额外维护一个单调递增的版本号(或时间戳)。
- 判定逻辑:
(当前值 == 预期值) && (当前版本号 == 预期版本号)。 - 即使值从 A 变回了 A,版本号也会从 1 变为 3,CAS 依然会检测到变化并拦截。
四、 推荐实现:AtomicStampedReference
在 Java 中,我们不需要手动通过 AtomicReference + AtomicInteger 来维护这种关系。因为手动维护无法保证“值+版本号”两者同时更新的原子性!!!
Java 并发包提供了 AtomicStampedReference<V>,其底层设计极其精妙:
1. 内部类 Pair
它将实际引用 reference 和版本戳 stamp 封装在一个静态内部类 Pair 中:

2. CAS 对象引用
AtomicStampedReference 内部维护一个 volatile Pair<V> pair 对象。
它的 CAS 操作实际上是对 Pair 对象引用的整体替换。由于替换一个对象的引用是原子指令,从而实现了“值+版本号”的联合原子更新。
五、 实战演示:拦截 ABA 过程
以下代码演示了如何利用 AtomicStampedReference 识别并拦截 ABA 问题:
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* 演示:使用 AtomicStampedReference 解决 CAS 的 ABA 问题
*/
public class ABASolution {
public static void main(String[] args) {
// 初始化:值为"A",初始版本号为1
AtomicStampedReference<String> asr = new AtomicStampedReference<>("A", 1);
// --- 线程2:模拟 ABA 操作 ---
new Thread(() -> {
int stamp = asr.getStamp(); // 获取初始版本号: 1
System.out.println("线程2:读取初始版本号 " + stamp);
// 第一次修改:A -> B,版本号 1 -> 2
asr.compareAndSet("A", "B", stamp, stamp + 1);
// 第二次修改:B -> A,版本号 2 -> 3
asr.compareAndSet("B", "A", asr.getStamp(), asr.getStamp() + 1);
System.out.println("线程2:完成 ABA 篡改,当前版本号为 " + asr.getStamp());
}).start();
// --- 线程1:受害线程尝试更新 ---
try {
// 确保线程2先完成 ABA 操作
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
int expectedStamp = 1; // 线程1 认为版本号还是 1
String expectedVal = "A";
// 尝试 CAS:预期值 A,预期版本 1 -> 更新为 C,版本 2
boolean success = asr.compareAndSet(expectedVal, "C", expectedStamp, expectedStamp + 1);
System.out.println("线程1 CAS 操作结果:" + success);
System.out.println("当前实际值:" + asr.getReference() + ",当前实际版本号:" + asr.getStamp());
}
}
运行结果分析:
虽然当前值确实是 “A”,但由于版本号已经从 1 变为了 3,线程 1 的 CAS 操作会返回 false。ABA 问题被完美解决。
六、 延伸:AtomicMarkableReference
如果不关心变量被修改了几次,只关心它是否被修改过,可以使用更轻量级的 AtomicMarkableReference。它内部维护的是一个 boolean 类型的标记位,逻辑与 AtomicStampedReference 一致,但开销略小。
总结
- ABA 问题是 CAS 在检测变量状态时的逻辑漏洞。
- 拒绝加锁:应通过引入版本号(Stamp)维持乐观锁的性能优势。
- 推荐工具:直接使用
AtomicStampedReference,它通过内部封装Pair对象,利用硬件级 CAS 保证了值与版本号更新的原子性。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)