小心!ABA 问题可能让你的并发代码悄悄出错

原文来自于: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 也可能酿大祸。

别忘了------并发编程是一门"踩坑学",踩得多了,经验自然就来了。

相关推荐
哞哞不熬夜2 小时前
JavaEE--SpringBoot
java·spring boot·java-ee
程序猿DD3 小时前
Spring Boot 4 与 Spring Framework 7 全面解析:新特性、升级要点与实战指南
java·spring boot·后端
用户6120414922133 小时前
jsp+servlet做的咖啡品牌管理后台
java·前端·后端
汇匠源3 小时前
基于springboot家政、上门服务、Java源码系统功能结构
java·spring boot·后端
tuokuac3 小时前
异常:java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter
java
麦兜*3 小时前
Spring Boot集群 集成Nginx配置:负载均衡+静态资源分离实战
java·spring boot·后端·nginx·spring·缓存·负载均衡
一叶飘零_sweeeet3 小时前
攻克 大 Excel 上传难题:从异步处理到并发去重的全链路解决方案
java·excel·大文件上传
huan19933 小时前
Java中实现html转pdf
java