ABA问题是CAS因只校验值相等而忽略中间修改导致的逻辑误判,典型场景包括无锁栈节点复用、资源池对象回收及版本号缺失的乐观更新,根源在于硬件CAS指令仅做数值比对、不记录修改历史。

要真正吃透 CAS 和 ABA 问题,得从“机制本质→问题根源→真实影响→落地解法”四层推进,不能只背定义。面试官想看的不是你复述“ABA 是值变回来导致误判”,而是你能说清:为什么 Java 里 AtomicInteger 很少出 ABA,但 ConcurrentLinkedQueue 却必须防?为什么加版本号就能破局?下面直击重点。
CAS 不是语法,是 CPU 指令+JVM 封装的组合
别被 Atomic* 类的简洁 API 迷惑——它背后没有锁、没有 synchronized,靠的是:
- x86 架构的 cmpxchg 指令(或 ARM 的 ldrex/strex),由硬件保证“读-比-写”三步原子执行;
- JVM 通过 Unsafe.compareAndSwapInt 等方法暴露该能力,AtomicInteger.incrementAndGet() 就是循环调用它直到成功;
- CAS 成功只取决于“当前内存值 == 预期旧值”,不关心谁改的、为什么改、中间有没有波动——这个“纯值校验”特性,就是 ABA 的温床。
ABA 不是 bug,是逻辑错位:表面没变,实际已脏
关键要理解:ABA 问题的危害不在数值错误,而在状态语义丢失。比如:
- 无锁栈中,节点 A 被弹出(A → null),内存回收后又被新节点复用地址,再压入栈顶(null → A);
- 线程 1 拿着旧的 A 节点引用做 CAS,发现栈顶还是 A,就认为“没人动过栈”,直接更新指针——结果跳过了中间已被替换的真实节点,链表断裂;
- 而 AtomicInteger 计数器很少真出 ABA,因为整型值从 100→150→100 没业务意义,但对象引用复用地址在并发结构里很危险。
解决 ABA,核心思路是给值“打时间戳”或“加版本号”
Java 提供了两个官方工具,不是为了炫技,而是针对不同场景:
立即学习“Java免费学习笔记(深入)”;
- AtomicStampedReference:把变量值和一个 int 版本号绑成 pair,CAS 同时校验值 + 版本号。每次修改值,版本号自增,哪怕值变回原样,版本号已不同,CAS 失败;
- AtomicMarkableReference:用一个 boolean 标志位代替版本号,适合只需区分“是否被修改过”的二元场景(如标记节点是否已删除);
- 注意:不要自己封装版本号字段——多个字段无法原子更新,必须用 JDK 提供的原子引用类,它们内部用 Unsafe 实现了 pair 的原子读写。
面试常挖的细节,必须脱口而出
光懂原理不够,这几个点一问就露馅:
- Unsafe 在 JDK 9+ 默认不可反射访问,所以别想着绕过 Atomic* 自己调 compareAndSwapObject;
- ConcurrentLinkedQueue 内部就用 AtomicStampedReference 防 ABA,不是靠运气;
- ABA 不一定报错或崩溃,它更可能造成数据跳过、任务丢失、状态不一致等静默逻辑错误;
- “乐观锁”是概念,CAS 是实现手段之一;数据库 version 字段、Redis 的 getset + 条件更新,都是乐观锁思想,和 Java CAS 本质同源。


















