【面试题-多线程】什么是 ABA 问题,怎么产生的,怎么解决?
一篇吃透CAS的ABA问题:成因、风险与全套解决方案
前言💡
并发编程中我们经常使用CAS(Compare And Swap)实现无锁并发,相比重量级synchronized拥有更高吞吐量。很多开发者熟练使用CAS,却忽略它经典的ABA漏洞。
本文循序渐进讲清楚:
- 什么是
ABA问题 ABA如何产生、会造成什么事故- 主流解决方案原理 + 代码示例
- 方案优缺点横向对比
一、什么是CAS?快速回顾📚
CAS全称比较并交换,是乐观锁底层原子操作,操作分为两步:
- 比较:判断内存当前值 == 预期旧值
- 交换:相等则更新为新值;不相等直接失败
伪代码逻辑:
if(内存值==预期旧值){内存值=新值;returntrue;}else{returnfalse;}注意:原生
CAS只对比数值本身,这就是ABA问题的根源!
二、什么是ABA问题?🤔
ABA定义:线程1读取变量值为A,在线程1执行CAS前;变量被其他线程修改为B,随后又被改回A。线程1执行CAS时发现值依旧是A,认为变量没有变动,执行更新,最终引发数据异常。
简单一句话:值看上去没变,但中间被篡改过!
ABA完整时序流程图
三、ABA问题真实场景举例🧨
最经典场景:并发栈无锁操作(单向链表实现栈)
栈结构:A → B → C,栈顶 = A
- 线程T1准备弹出栈顶A,读取头结点A;准备执行CAS
- T1暂停;线程T2执行出栈:弹出A,栈变成
B→C - T2继续把A入栈,栈变回
A→B→C - T1恢复执行CAS,发现栈顶依旧是A,执行出栈
最终链表指针错乱,出现内存丢失、循环链表、数据泄露严重bug!
风险总结
原生CAS只校验数值,无法识别中间变更历史。
简单类型(数字)场景ABA危害不直观;链表、对象引用等基于指针的结构,ABA极易引发灾难性BUG。
四、ABA问题核心产生原因✅
- 基础版本
CAS仅比较数据值,不记录变量修改历史; - 变量经历:
A → B → A,最终数值复原; - 线程无法区分:【从未修改的A】 vs 【被修改后恢复的A】;
核心痛点:缺少版本标记。
五、ABA问题主流解决方案🛠️
方案1:带版本号的CAS(乐观锁版本号机制)
原理
不再只对比数据本身,每条数据附带一个自增version版本号:
- 每次成功修改变量,版本号
version = version + 1 - CAS条件:旧数据 + 旧版本号 同时匹配,才能更新
时序图:
Java中对应实现:AtomicStampedReference(携带戳stamp,等价版本号)
// 初始化 初始值A,版本戳stamp=1AtomicStampedReference<String>atomicRef=newAtomicStampedReference<>("A",1);intoldStamp=atomicRef.getStamp();StringoldVal=atomicRef.getReference();// 其他线程修改A→B→A,stamp自增为3// CAS:值A,版本1,期望匹配booleansuccess=atomicRef.compareAndSet("A","NEW",oldStamp,oldStamp+1);// 版本不匹配,返回false,成功规避ABA方案2:AtomicMarkableReference(布尔标记)
存储一个boolean mark标记,只能区分「是否被修改过」,无法记录修改次数。
局限性:多次修改后标记会反复翻转,不能彻底杜绝ABA,适用场景有限。
方案3:数据库乐观锁 version字段
业务开发最常用!数据表增加version字段
-- 更新时带上版本条件UPDATEuserSETbalance=balance-100,version=version+1WHEREid=1ANDversion=#{oldVersion};方案4:使用重量级锁(兜底方案)
直接抛弃无锁CAS,使用synchronized/ReentrantLock,同一时间只允许一个线程操作,从根源杜绝并发竞争。
缺点:并发性能下降,失去无锁优势。
方案5:使用GC+不变对象(不推荐)
每次修改新建对象,不复用旧对象引用,成本极高,极少使用。
六、方案优劣横向对比📊
| 方案 | 能否彻底解决ABA | 性能 | 适用场景 |
|---|---|---|---|
| AtomicStampedReference(版本戳) | ✅ 完全解决 | 高 | Java内存无锁并发 |
| AtomicMarkableReference | ❌ 不完全解决 | 高 | 只关心是否改动,不关心次数 |
| 数据库version乐观锁 | ✅ 完全解决 | 中等 | 数据库业务并发更新 |
| synchronized 互斥锁 | ✅ 完全解决 | 较低 | 简单并发,追求稳定不追求极致吞吐 |
七、常见误区澄清⚠️
AtomicInteger / AtomicLong 会遇到ABA吗?
会!基础原子类只有数值CAS,没有版本戳。
但普通数值加减场景ABA很难触发严重故障;链表、队列无锁实现必须规避。只要用CAS就一定要处理ABA?
不是。如果业务允许「中间被修改又复原」这种场景,可以不用处理。
但开发通用无锁数据结构(队列、栈)必须防范。stamp版本号会溢出吗?
long型版本戳溢出周期极长,工程上基本不用考虑溢出问题。
八、总结📝
ABA本质:变量先A→B→A,原生CAS只对比数值,无法感知中间变更;- 最大风险:链表、无锁队列等基于引用操作的数据结构;
- 标准根治方案:增加自增版本号,使用带版本校验的CAS;Java优先使用
AtomicStampedReference;数据库增加version乐观锁; - 简单并发场景,也可以直接使用互斥锁规避并发竞争。
日常业务代码很少手写无锁链表,ABA问题遇到概率不高;但学习并发、阅读JDK源码(Concurrent相关容器)必须理解这个经典并发缺陷。
