小心!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 也可能酿大祸。

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

相关推荐
逍遥德16 小时前
Java 锁(线程间)和数据库锁(事务间)对比详解
java·数据库·sql·高并发·锁机制
gwjcloud16 小时前
Docker详解
java·docker·容器
河阿里16 小时前
Java-JWT令牌技术深度指南
java·开发语言
WiChP17 小时前
【V0.1B6】从零开始的2D游戏引擎开发之路
java·log4j·游戏引擎
leaves falling17 小时前
C/C++ 的内存管理,函数栈帧详讲
java·c语言·c++
文静小土豆17 小时前
Java 应用上 K8s 全指南:从部署到治理的生产级实践
java·开发语言·kubernetes
zhimingwen17 小时前
初探 Java 後端開發:解決 macOS 環境下 Spring Boot 項目啟動的各類「坑」
java·spring boot
Rsun0455118 小时前
3、Java 工厂方法模式从入门到实战
java·开发语言·工厂方法模式
田梓燊18 小时前
leetcode 142
android·java·leetcode
亚空间仓鼠18 小时前
Ansible之Playbook(三):变量应用
java·前端·ansible