ABA问题根本原因是CAS只校验值是否等于预期而忽略中间变化;需满足共享变量初值为A、线程1读取后阻塞、线程2完成A→B→A三条件;AtomicReference等因无历史追踪能力无法解决;主流方案是AtomicStampedReference(加版本号)或AtomicMarkableReference(加标记)。

Java里出现ABA问题,根本原因在于CAS(Compare-And-Swap)操作只校验“值是否等于预期”,而完全忽略“这个值有没有被改过又改回来”。表面看没变,实际中间状态已丢失——就像你出门前桌上放着100元,回来还剩100元,但可能被别人拿走又偷偷放回,你却浑然不知。
ABA问题是怎么发生的
它需要三个关键条件同时满足:
- 一个共享变量初始值为A(比如AtomicInteger设为100)
- 线程1读取到A,准备用CAS更新,但中途被阻塞或延迟
- 线程2快速执行两次修改:A → B → A(例如100 → 50 → 100),CAS仍能成功,因为最终值还是A
这时线程1的CAS会误判“没人动过”,继续执行后续逻辑,但实际业务语义可能已错乱——比如库存扣减、账户转账、链表节点回收等场景,中间B态可能代表资源已被分配或释放。
为什么普通AtomicReference解决不了
AtomicReference、AtomicInteger这些类的compareAndSet方法只传两个参数:旧值和新值。它没有能力记住“这个A是不是原来的那个A”。硬件层面的CAS指令本身就不带历史追踪能力,Java只是封装了这一底层行为。
立即学习“Java免费学习笔记(深入)”;
换句话说:CAS是“近视眼”,只看当前快照,不记来路。
主流解决方案:加维度识别变化
核心思路是给数据加上一个可变的“身份标识”,让A→B→A的过程留下痕迹。常用两种方式:
- AtomicStampedReference:搭配整型版本号(stamp),每次修改都递增stamp。CAS变成四参数操作:旧引用、新引用、旧stamp、新stamp。哪怕值回到A,stamp从1变成3,CAS就失败
- AtomicMarkableReference:用一个布尔标记(mark)表示“是否被修改过”。适合只需区分“改过”和“没改过”的二元场景,比版本号更轻量
两者都要求你在读取时同时获取值和标识,在更新时一并校验——不能只读值、忽略stamp或mark。
其他可行但需权衡的方式
除了原子类自带方案,还有几种思路,但各有适用边界:
- 加锁同步:用synchronized或ReentrantLock串行化访问。彻底规避ABA,但牺牲并发性能,违背无锁设计初衷
- 数据库乐观锁+version字段:在持久层用UPDATE ... WHERE id = ? AND version = ?,原理同AtomicStampedReference,适用于涉及DB的业务流程
- 时间戳替代版本号:用System.nanoTime()等生成唯一递增戳,注意高并发下纳秒级也可能冲突,需配合CAS重试
基本上就这些。关键不是选哪种,而是意识到:只要用CAS且业务对“中间态敏感”,就必须补上这个维度——否则ABA不是会不会发生的问题,而是何时暴露的问题。










