CAS 原理与 ABA 问题
候选人小冯在面试京东 P6 时,面试官问道:
"并发编程中,AtomicInteger 的 incrementAndGet() 是怎么实现的?"
小冯说:"用了 CAS..."面试官追问:"什么是 CAS?它的底层是怎么实现的?"
小冯说:"Compare And Swap..."面试官继续追问:"CAS 有什么问题?ABA 问题是什么?怎么解决?"
小冯支支吾吾答不上来了...
一、核心问题:CAS 原理 🔴
1.1 问题拆解
1.2 ❌ 错误示范
候选人原话 A:"CAS 就是乐观锁,不需要加锁,性能更好。"
问题诊断:这个说法太笼统。CAS 的"乐观"体现在假设没有竞争,但如果竞争激烈(多个线程同时 CAS 失败),会导致大量自旋重试,反而比加锁更慢。乐观锁适用于低竞争场景。
候选人原话 B:"ABA 问题就是值从 A 变成 B 再变回 A,CAS 会认为没变。"
问题诊断:这个理解正确,但不够完整。ABA 问题的真正危害在于:如果值从 A 变成 B 再变回 A,在这个过程中如果有其他线程做了一些依赖 A 状态的中间操作,这些操作的结果会被覆盖。
1.3 标准回答
P5 级别:CAS 概念与原子类应用
CAS(Compare And Swap):
CAS 是一种无锁算法,包含三个操作数:
- 内存位置 V:要更新的变量的地址
- 期望值 A:认为当前应该持有的值
- 新值 B:要写入的新值
CAS 的语义是:只有当 V 的当前值等于 A 时,才将 V 更新为 B;否则什么都不做。整个过程是原子的。
AtomicInteger 的 incrementAndGet() 实现:
这个 do-while 循环会一直重试,直到 CAS 成功。线程不会阻塞,而是在用户态自旋。
P6 级别:底层实现与 CPU 指令
x86 架构的 CAS 实现:
CAS 在 x86 上对应
lock cmpxchg指令(Compare and Exchange):
lock前缀的作用:
- 总线锁:在多核 CPU 上,发送 LOCK# 信号锁定总线,确保当前内存操作是原子的
- 缓存一致性:使其他 CPU 的缓存行失效(类似 MESI 协议的 invalidate 广播)
- 内存屏障:禁止后续指令重排序到 lock 操作之前
为什么需要自旋而不是一次成功?
因为 CAS 是乐观的——假设大多数时候没有竞争。但如果竞争存在(多个线程同时修改同一变量),CAS 会失败并重试。这种自旋 + CAS 的组合称为 CAS 循环。
CAS 的性能特征:
- 单次 CAS 操作约 10~50 纳秒(跨核通信约 100 纳秒)
- 竞争激烈时,大量线程同时自旋 → CPU 空转 → 性能退化
- 适合场景:低竞争、单个变量更新、多读少写
P7 级别:ABA 问题与三种解决方案
ABA 问题的场景:
如果线程 A 读取栈顶为 A,其他线程 P 将栈顶改为 B 再改为 A,此时线程 A 的 CAS 仍然成功(因为值为 A),但栈的结构可能已经被破坏。
解决方案一:版本号(Timestamp/Stamp)
每次修改不仅改变值,还增加版本号:
JDK 的实现:AtomicStampedReference
解决方案二:AtomicMarkableReference
使用布尔标记而非版本号,适合"一次性"操作(删除后不再使用):
解决方案三:Double CAS(双 CAS)
对两个相关变量同时 CAS,确保它们的原子性。但这不能从根本上解决 ABA 问题——如果两个变量的版本号同步增加,仍然可能"巧合地"通过检查。
【面试官心理】 这道题我能问到 P7 级别,是因为 CAS 涉及 CPU 指令、并发算法、ABA 问题三个维度。能说出 lock cmpxchg 指令细节的候选人说明他对底层有了解。能说出 ABA 问题的具体场景(如栈的 push/pop)的候选人说明他有实战经验,而不是只会背概念。
1.4 追问升级
追问 1:CAS 和 synchronized 的性能差异?
一般规律:自旋超过 10 次(或阈值可动态调整)仍未成功,说明竞争激烈,应该切换到阻塞模式(轻量锁膨胀为重量锁)。
追问 2:AtomicInteger 和 LongAdder 哪个更好?
- 低竞争:
AtomicInteger更好,因为 LongAdder 有额外的 Cell 数组开销- 高竞争:
LongAdder更好,因为分段热点减少竞争- 需要原子加减:
AtomicLong(JDK 8)或LongAdder(JDK 8+)- 需要复合操作:
AtomicInteger(如updateAndGet()、accumulateAndGet())
二、CAS 的 ABA 实战场景 🟡
2.1 栈的并发问题
解决方案:使用 AtomicStampedReference,每次 push/stack 操作都递增 stamp。
2.2 链表操作中的 ABA
在无锁链表中删除节点时,如果被删除节点被其他线程重新插入链表,可能导致数据丢失。JDK 的 ConcurrentLinkedQueue 使用 CAS + p.markAsUnlinked() 标记避免了这个问题。
三、生产避坑
3.1 LongAdder 的 sum() 不是实时值
风险:如果用 LongAdder.sum() 作为精确计数(如库存扣减),可能有误差。应该用 LongAdder.sumThenReset() 或考虑使用 AtomicLong。
3.2 AtomicReference 的泛型陷阱
AtomicReference<User> 只保证引用的原子性,不保证 User 内部字段的原子性:
面试加分点:能说出"JDK 9 引入了 VarHandle,它提供了比 CAS 更细粒度的内存操作(setAcquire、setOpaque、setVolatile)和更强的原子性保证(fullFence)",说明他对 JUC 的演进有跟进。
面试陷阱:被问到"AtomicReference 的 compareAndSet 和 CAS 的关系",很多人会说"compareAndSet 就是 CAS"。准确地说:compareAndSet 是 Unsafe.compareAndSwapObject 的包装,但 CAS 操作不仅用于 compareAndSet——AtomicInteger.incrementAndGet() 内部也是用 getAndAddInt 的 CAS 循环。CAS 是一种思想,compareAndSet 是这个思想的具体 API 实现。