原文来自于:zha-ge.cn/java/92
小心!ABA 问题可能让你的并发代码悄悄出错
有时候写代码,跟打怪升级一样。你战胜了线程安全,觉得自己稳如老狗,结果一回头,被"后面的小怪"------ABA 问题,背刺了一刀。这事儿就发生在前不久......
我刚写了个多线程下并发安全的队列,CAS 操作用的贼流畅,朋友还夸了句"哥们你这代码,不愧是自学成才啊!" 结果所有测试都过了,业务上了线才出事。 线上日志开始偷偷给我发红包:
"队列里的数据,咋出现鬼影了?"
隐隐约约,感觉背后凉嗖嗖的。ABA,终于出场了。
故事的开头------CAS 的光与影
说到 Java 并发,CAS(Compare And Swap)基本算得上老伙计。只要能不用锁,谁都愿意 CAS 一把,比如 AtomicInteger、ConcurrentLinkedQueue 这些场景。 比如下面:
java
do {
oldValue = atomicRef.get();
newValue = ... // 新值的计算方式
} while (!atomicRef.compareAndSet(oldValue, newValue));
看起来像极了"乐观派":假如没人动过,那我就小步快跑地修改一下。
那问题就来了,难道它天生完美么?
终于踩雷------ABA 问题来了
其实我一开始也没想太多。直到那个周五下午,测试同学在工位旁拍我肩膀,说
"你这队列咋从队头读出来的数据明明删掉了,后面又冒出来一份?"
哦呦,出 BUG 了。 翻代码、看历史日志,才意识到遇上了传说中的"ABA 问题":
- 线程1准备把队头元素 A 出队
- 这时线程2 把 A 出队,又把一个"新"A又加回队头(内存地址复用,值一样)
- 线程1晃晃悠悠地用 CAS 校验,发现"嘿,还是 A!"
- 于是线程1以为啥都没变,放心大胆地改了队列指针,出大事了......
本质问题: CAS 只能比对当前值,如果"值"没变,但实际对象可能早就变了个来回。就像你早上见到一个人叫王二狗,中午他走了,傍晚又回来,你还以为是原来的二狗。殊不知,早上那个二狗已经被外星人带走了!
踩坑瞬间
过程大致如下:
- 写测试时,一切 OK(毕竟操作速度赶不上真实环境)
- 上线后,偶发行为诡异
- 日志里明明处理过的数据又莫名其妙回来了
- 大量排查才意识到:CAS 校验的只是"值表象",没有识别"身份"
具体一段问题代码(类似这样),大家感受一下:
java
Node<E> first = head.get();
// ... 多线程可能偷偷改了 head
head.compareAndSet(first, first.next);
表面看起来 CAS 很安全,谁改动了就让我重试。实际上,线程2把节点先删后加,first还是原样,CAS 根本发现不了"灵魂已切换"。
怎么破?版本号走起
难道能怎么办?难不成直接放弃乐观锁,换回 synchronized? 其实不用急,小聪明还是有的。
比如,Java 里的 AtomicStampedReference
就是专门防 ABA 的,给每一份数据配上"代号"------版本号。
这样CAS校验就变"值 + 版本号"一起核查,只要版本号变化了,你就别想糊弄我。
java
int[] stampHolder = new int[1];
V ref = atomicStampedRef.get(stampHolder);
// ...
boolean updated = atomicStampedRef.compareAndSet(
ref, newRef, stampHolder[0], stampHolder[0] + 1
);
这下就算线程偷偷把数据ABABA复原,版本号也早换好几个来回了。
实战用上,队列再也没出过鬼影。
经验启示
场景选择要谨慎 CAS 类结构(AtomicXXX)非常适合计数器、队列指针这种场景,但涉及到"引用对象"时,要特别小心 ABA。 如果你只是在维护一个简单数值,比如线程数、ID 自增,ABA 问题基本不用操心;可一旦涉及到复杂链表、队列,就得提前设计防护。
底层库已经帮你挡了一部分坑 你常用的 ConcurrentLinkedQueue、ConcurrentSkipListMap,这些 JDK 并发容器内部其实早就用了各种骚操作(比如版本戳、冗余校验)。但要是你手撸数据结构,就得自己兜底。
性能和安全的权衡 AtomicStampedReference 虽然解决了 ABA,但带来的额外存储和版本号管理开销,也不是白送的。高性能场景要仔细评估,别一味套上去。
面试官杀手锏问题
问:ABA 问题在实际工程中严重吗?
答法思路:
先承认 CAS 高效,但指出"在链表、栈、队列这类需要判断引用身份的结构里,ABA 是潜在风险"。
再补一句:"在 JDK 并发包里,很多地方已经通过版本戳、冗余校验绕过了,所以日常开发不常撞上,但手写并发结构一定要防范。" 👉 这样既显示你懂原理,也不会被认为危言耸听。
最后我想说的是
ABA 问题就是个隐形的"并发彩蛋":表面风平浪静,背地里暗潮涌动。 它提醒我们:
乐观锁并不是万能的;
多线程 Bug,往往不是测试能立马测出来的,而是上线后"偶尔抽风";
真正写并发代码,要多动脑子,不然哪怕一行小小的 CAS 也可能酿大祸。
别忘了------并发编程是一门"踩坑学",踩得多了,经验自然就来了。