CAS 详解
CAS(Compare-And-Swap)是乐观并发控制的硬件基石:比较内存当前值与期望值,相等才原子地换成新值。它不需要互斥就让单变量更新具备原子性,JUC 的原子类、AQS 的 state 变更都建立在这条指令之上。
它解决什么问题
悲观锁的思路是「先独占,再操作」:进入临界区前必须拿到锁,拿不到就阻塞。阻塞意味着上下文切换,竞争一多,调度开销常常超过临界区本身的耗时。
乐观锁反过来:先假设没有冲突,直接动手;提交前校验一下有没有被别人改过,改过了就重来。CAS 正是这个「校验并提交」动作的原子化实现。它回答三个问题:内存位置 V、期望值 A、新值 B——仅当 V 当前仍等于 A 时才写入 B,否则什么都不做。整个判断与写入由硬件保证不可中断,于是单变量的读-改-写不再需要锁。
代价也随之而来:CAS 只保护那一次比较与交换,多次 CAS 之间别的线程随意穿插,它不承诺业务层面的整体原子性;失败要重试,重试要烧 CPU。理解 CAS 就是理解这笔交换的边界。
核心原理
从 Java 方法到 CPU 指令
Java 侧表达 CAS 的入口是 sun.misc.Unsafe(JDK 9 之后更推荐 VarHandle),它提供三个 native 方法:
boolean compareAndSwapObject(Object o, long offset, Object expected, Object x); |
参数里的 offset 是字段在对象内存布局中的偏移量,配合对象引用即可定位到要修改的那块内存。HotSpot 会把这些调用识别为 intrinsic,在 JIT 编译时直接映射到目标平台的原子指令——x86 上是带 LOCK 前缀的 CMPXCHG,ARM 上是对应的 LL/SC 指令对。LOCK 前缀保证指令执行期间该缓存行的访问不被其他核插入,这就是「原子」二字的物理来源。具体实现取决于 JVM 与 CPU 架构,并非某种固定的 JNI 汇编写法。
AtomicInteger 的标准套路
java.util.concurrent.atomic 包下的原子类全都建立在同一套模式上。以 AtomicInteger(JDK 8)为例:
private static final Unsafe unsafe = Unsafe.getUnsafe(); |
三个要素一目了然:value 用 volatile 修饰保证可见性,valueOffset 在静态块里算好,compareAndSet 一行转发给 Unsafe。自增这样的复合操作则靠自旋完成:
public final int getAndAddInt(Object o, long offset, int delta) { |
循环体每次先 volatile 读到当前值 v,再尝试 CAS 写成 v+delta;失败说明期间被别人改过,读新值再来一轮,直到成功。这就是「CAS + 自旋」的万能模板,也是自旋锁的最小形态。
深入机制
ABA 问题与版本戳
CAS 只比较「值是否还等于 A」,可值等于 A 不代表没动过。想象一个用 CAS 维护的无锁栈,栈顶指针从 A 节点出发,期间被另一线程弹出 A、压入 B、又压回一个地址复用的 A——比较时看到的还是 A,但栈的内部结构早已面目全非。这就是 ABA 问题:对纯数值通常无害,对依赖「中间没发生过变化」这一前提的结构(链表、栈)可能是致命的。
标准解法是给每次变更配一个单调递增的版本号/时间戳,比较时连值带版本一起校验。JDK 的 AtomicStampedReference 就是这个思路:
public boolean compareAndSet(V expectedReference, |
引用与 stamp 被打包进一个不可变的 Pair,CAS 换的是整个 Pair 对象:值相同但 stamp 不同,照样判定失败。JDK 还提供只比较版本的 AtomicMarkableReference。工程上还有一条绕开 ABA 的路:像 ConcurrentLinkedQueue 那样每次变更都换全新节点对象,靠对象唯一性而非值比较来判断「没动过」。
自旋的代价
CAS 失败必须重试,高竞争下重试率飙升,CPU 大量时间花在空转上。两个缓解手段:
pause指令:自旋循环中插入它,一方面让流水线稍作停顿降低功耗,另一方面避免退出循环时因内存顺序冲突导致流水线清空。HotSpot 的自旋逻辑已内置。- 退避(backoff)与降级:连续失败时指数退避,或者干脆在高竞争场景换成分段结构或锁。CAS 的优势区间是低中竞争,不是万能替代。
另外记住 CAS 的天然边界:一条指令只护得住一个内存位置。要原子地更新多个字段,要么把它们封装进一个不可变对象用 AtomicReference 整体替换,要么回到锁。
LongAdder:把热点拆开
AtomicLong 在高并发计数上的瓶颈很物理:所有线程对同一个缓存行做 CAS,失败率随线程数上升,缓存行在核间反复失效。LongAdder 的答案是分段——内部维护一个 base 值加一组 Cell 单元:
- 无竞争或低竞争时,直接 CAS 更新
base,开销与AtomicLong相当; - 检测到竞争时,线程按哈希分散到不同
Cell上各自累加,热点从一行摊到多行; - 读取时
sum()把base和所有Cell加总。
注意 sum() 返回的是调用瞬间的快照近似:汇总期间可能有并发累加,结果不保证强一致。统计类指标无所谓,当精确余额用就错了。
工程实践
- 按竞争强度选型:低到中等竞争用
AtomicInteger/AtomicLong足够;线程多、写密集(如全局限流计数、QPS 统计)换LongAdder;需要读到的值实时精确,只能接受 AtomicLong 的开销或加锁。 - 多变量不变量别硬上 CAS:两个字段必须同时满足某种关系时,封装成不可变对象用
AtomicReference整体替换,或直接上锁。硬拆成多次 CAS 必然出现中间态。 - 警惕 ABA 的隐蔽性:数值自增自减通常无感,但涉及对象复用(内存池、无锁数据结构)时,先问自己「值相等能否推出没变化」,答不上来就加版本戳。
- 别让 CAS 做重活:自旋临界区里若夹杂慢操作(IO、锁),重试风暴会放大成 CPU 打满,比锁更难排查。
对比与权衡
| 维度 | synchronized | CAS 原子类(AtomicLong) | LongAdder |
|---|---|---|---|
| 并发策略 | 互斥阻塞 | 乐观重试,单热点 | 乐观重试,分段摊热点 |
| 低竞争开销 | 较高(有锁获取释放路径) | 低 | 低 |
| 高竞争表现 | 线程排队,吞吐受限 | 自旋空转,CPU 飙升 | 分散冲突,吞吐高 |
| 读一致性 | 临界区内强一致 | 读到的是某一时刻的真实值 | sum() 为弱一致快照 |
| 内存占用 | 无额外结构 | 一个值 | base + Cell 数组,竞争越大占用越多 |
| 典型场景 | 复杂不变量、多字段 | 低中竞争计数、状态翻转 | 高并发统计、监控计数 |
一句话:CAS 赢在无争用时的轻量,LongAdder 赢在高争用时的可扩展,锁赢在语义的普适与安全。
面试视角
CAS 是怎么保证原子性的? Java 层通过 Unsafe.compareAndSwapXxx 表达语义,HotSpot 将其编译为目标平台的原子指令,如 x86 的 LOCK CMPXCHG。LOCK 前缀保证比较与交换之间其他核无法干预该缓存行,原子性由硬件提供。
ABA 问题怎么解决? 本质是「值相等推不出没变化」。方案是给每次变更附加版本号或时间戳,比较时值与版本一起校验,JDK 对应 AtomicStampedReference/AtomicMarkableReference;或者像无锁队列那样每次替换都用新对象,靠对象身份规避。
CAS 有什么缺点? 三个:一是 ABA;二是高竞争下自旋重试开销大;三是只能原子更新单个变量,多字段不变量需要封装对象或回到锁。
LongAdder 为什么比 AtomicLong 快? AtomicLong 让所有线程竞争同一个缓存行,冲突率随线程数线性上升。LongAdder 把累加分散到 base 与多个 Cell 单元,大幅降低单行冲突,读时再汇总。代价是 sum() 只是弱一致快照,且占用更多内存。
CAS 和 synchronized 怎么选? 临界区小、竞争不激烈、操作是单变量更新时用 CAS 原子类;临界区涉及多变量不变量、逻辑复杂或竞争激烈到自旋空转时,synchronized(现代 JDK 有锁升级优化)往往更稳也更省 CPU。
小结
- CAS 是乐观锁的指令级实现:比较期望值再交换,原子性来自
LOCK CMPXCHG这类硬件指令。 - Java 原子类的套路是「volatile 值 + Unsafe 偏移量 + 自旋重试」,JUC 大量组件都踩着这套模板。
- ABA 用版本戳解决;高竞争用 LongAdder 分段摊平热点;多字段一致性回到锁或整体替换。
- 选型看竞争强度与一致性要求,CAS 不是锁的全面替代,而是低中竞争区间的性能武器。

