CAS自旋锁本身不引发ABA问题,但因其仅比较值是否相等,易在A→B→A场景下误判;需同时满足共享变量可复用、线程2改回原值、线程1完成CAS三个条件。

CAS 自旋锁本身不会直接“带来”ABA问题,但它是ABA问题发生的典型场景——因为CAS只比对值是否相等,不关心这个值被改过几次、中间经历过什么。一旦某个变量从A→B→A,CAS会误判为“没变过”,从而错误地完成交换,这就是ABA问题。
ABA问题的典型发生条件
必须同时满足三个条件:
- 共享变量是引用类型(比如指向一个节点的对象)或可被反复复用的基本类型值(如整数计数器被重置)
- 线程1读取到值A,准备CAS;此时线程2抢先进来,把A改成B,再改回A
- 线程1执行CAS,发现仍是A,于是成功交换——但语义上,这已经不是“原封不动的A”,而是“被篡改后又伪装成A”的状态
主流解决方案:版本号机制(AtomicStampedReference)
核心思路是给数据“加戳”:不仅存值,还存一个随每次修改递增的版本号。CAS时同时比较值和版本号,二者都匹配才算通过。
Java中对应类是AtomicStampedReference,用法示例:
立即学习“Java免费学习笔记(深入)”;
AtomicStampedReference<Node> ref = new AtomicStampedReference<>(head, 0); int[] stamp = new int[1]; Node current = ref.get(stamp); // 同时取出当前值和版本号 int oldStamp = stamp[0]; // 尝试将current换成newNode,要求版本号仍为oldStamp boolean success = ref.compareAndSet(current, newNode, oldStamp, oldStamp + 1);
注意:版本号不能无限增长,需防止溢出;实际项目中常配合循环重试逻辑使用。
替代方案:延迟回收(如Doug Lea在ConcurrentLinkedQueue中的做法)
针对指针类ABA(如链表节点被回收又重新分配),不靠版本号,而是让旧节点“暂时不可被重用”:
- 节点删除时不立即释放内存,而是放入本地无锁回收队列
- 等待所有可能正在CAS的线程都完成一轮遍历后再真正回收
- 依赖ThreadLocal + 周期性屏障(如GC safepoint)保证安全窗口
这种方式避免了原子操作额外携带版本字段的开销,但实现复杂,JUC中部分队列类内部采用。
哪些情况其实不用管ABA?
不是所有CAS场景都需要防范ABA:
- 纯数值计算(如AtomicInteger做计数器):A→B→A只是巧合,不影响业务逻辑(+1两次还是+2)
- 不可变对象或仅追加结构(如日志写入位置):值只会单调递增,不可能回退到旧值
- 业务层已通过其他方式校验(如带时间戳的订单状态更新)
关键看“值相等”是否等价于“状态未被干扰”。不等价时,才需要版本号或延迟回收。


















