什么是 Java 中的 ABA 问题?
- 标签:Java并发、Java
🏛 业务架构师
ABA 问题:线程 1 读到值 A;期间线程 2 把 A 改成 B 又改回 A;线程 1 的 CAS 检查"还是 A"判定成功——值相同,但世界已经变了。CAS 只比数值,不知道"中间发生过什么"。
业务上的真实风险画像(比算法描述更重要):
- 资金/状态语义失效:账户因风控被冻结(A→冻结→解冻回到原值),另一个流程按"余额没变"放行了交易——余额相同不等于状态合法;
- 链表/堆指针场景(无锁算法的重建/复用):节点被移除又复用,指针指回原位置但对象已换——纯内存数据结构的经典崩溃源;
- 取号业务:工单被处理完又退回,CAS 判定"还是待处理"重复处理——重复动作就是资损。
解法:给状态加"版本":
AtomicStampedReference(引用+版本戳)/AtomicMarkedReference;- 数据库层就是乐观锁 version 字段(
update ... where id=? and version=?)——版本号是业务状态机的标配。
我的架构规约:凡是状态流转类业务(订单状态、审批流、账户状态、库存占用),一律带版本/时间戳判断,"值相等"永远不能作为"没被改过"的证据——这是并发正确性的基础素养。
顺带一个正面判断:大部分普通数值场景(计数、累计)ABA 无害——值就是全部语义,回不回原值无所谓。别为所有 CAS 场景都上版本号,按语义敏感性分级,这才是工程判断而不是模式化堆砌。
一句话:ABA 的教训是**"数据相同 ≠ 状态相同"**——识别你的业务里哪些值背后藏着状态语义,给它们版本号。
🏗 系统架构师
ABA 是 CAS 的问题场景:值从 A 改成 B 又改回 A,CAS 检查时误判"没变过",操作成功但前提已失效。
危险场景举例:无锁链表——线程 1 要把头节点 A 换成 B(A.next),期间线程 2 把 A 删除又 new 了个一模一样值的新 A 放回头部(但内存对象不同、next 关系已变);线程 1 的 CAS 看到头还是"A",成功替换——但实际操作的链结构早已不是它以为的那个。钱账户 A→B→A 的"余额没变"也可能掩盖了两次真实变动。
解法:加版本号:
- AtomicStampedReference:引用 + int 版本戳,CAS 同时比对 (reference, stamp),每次变更 stamp+1——值"看起来一样"但版本变了,CAS 失败;
- AtomicMarkableReference:布尔标记的简化版;
- 数据库乐观锁的 version 字段同理。
工程判断(重要的收尾):ABA 是否要治理取决于业务语义——纯数值累加、状态机里"A 回来等价于没变"的场景无害;涉及内存身份(引用、链表结构)或变更历史敏感(审计、资金)时必须防。多数业务代码用 Atomic 计数器无需担心 ABA,但面试答出"什么时候需要防"比背定义更能体现深度。
💻 全栈开发专家
CAS 只比较"当前值是否等于我读到的旧值",无法感知"值从 A 改成 B 又改回 A"的过程——ABA 问题。
危害场景(必须举出具体例子):
java
// 无锁栈的典型事故
线程1 读到栈顶 A(下一个是 B),然后被挂起
线程2:pop A,pop B,再 push A(同一个对象复用或新 A)
线程1 恢复:CAS(top, A, B) 成功!但此时 B 的 next 已经不是当初的语义
// 结果:栈结构损坏,丢节点/成环日常业务里纯数值更新(计数、开关)遇到 ABA 大多无害;对象引用、链式结构、累计型语义("只要没被改过就...")必须防。
解决方案:
java
// AtomicStampedReference:带版本号(戳)
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);
int stamp = ref.getStamp();
// 别人改过再改回,stamp 也变了
ref.compareAndSet(100, 200, stamp, stamp + 1);数据库层的对应物就是乐观锁 version 字段:UPDATE ... SET version=version+1 WHERE id=? AND version=?——version 单调递增,天然免疫 ABA。
引申(体现深度):JDK 的 AtomicStampedReference 之外,LongAdder 干脆不比较(分段累加),ConcurrentLinkedQueue 用谨慎的 CAS + 变量范围收缩规避;绝大多数 ABA 防护在工程上都通过"版本号/时间戳进入比较域"实现。