CAS 乐观锁
CAS 是项乐观锁技术,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查 + 数据更新的原理是一样的。
三操作数模型讲清了 CAS 是什么,但工程上还需要三块:硬件如何保证 CAS 本身原子、Java 侧的入口(Unsafe/atomic/VarHandle)、以及 CAS 的三大代价(ABA、自旋开销、单变量局限)。本文补全。
硬件原子性:lock 前缀
CAS 在 x86 上对应 cmpxchg 指令并带 lock 前缀:lock 保证”比较+交换”在整个总线/缓存行层面原子执行(锁缓存行或总线锁),多核同时 CAS 同一地址时硬件串行化,只有一个成功。所以 CAS 的原子性不靠软件约定,而是 CPU 指令保证——这是所有无锁结构的物理地基。
Java 侧入口与 atomic 包
Unsafe.compareAndSwapInt(JDK8)/VarHandle.compareAndSet(JDK9+ 推荐):直接暴露硬件 CAS- atomic 类 = volatile 字段 + CAS 循环:
public final int incrementAndGet() { |
这个”读-算-CAS-失败重试”循环就是无锁原子操作的通用模板(CAS loop)。LongAdder 进一步把单点 CAS 分散为 Cell 数组(热点分片),高竞争下吞吐远超 AtomicInteger。
乐观 vs 悲观的代价模型
- 悲观锁:冲突时阻塞等待,切换/唤醒有内核开销;适合高竞争(等待比重试便宜)
- CAS 乐观:冲突时立即重试,无阻塞;适合低竞争(重试大概率一次成功)
竞争强度是选型分水岭:竞争极高时 CAS 自旋重试浪费 CPU(甚至 livelock),此时锁反而更优。atomic 类在”中低竞争计数/状态翻转”场景最优。
CAS 的三大代价
- ABA 问题:V 从 A 改 B 又改回 A,CAS 误判”未变”。对引用型结构(无锁栈/链表)可能踩到已回收节点;解法见版本号/AtomicStampedReference(下一篇详述)
- 自旋开销:长时间不成功的空转消耗 CPU;JDK 的自适应思路与 LongAdder 分片都是缓解手段
- 单变量原子局限:CAS 一次只保护一个内存位置;多变量不变式(转账 a-100 且 b+100)无法用单次 CAS 表达,需合并为一个不可变对象引用做 CAS(原子引用换整体),或退回锁
业务层乐观锁的同构性
数据库乐观锁与 CAS 同构:
UPDATE account SET balance = balance - 100, version = version + 1 |
version 即预期值 A,UPDATE 的原子性即硬件 CAS 的软件对应。理解这层同构,”乐观锁”在语言层与数据层就是同一个思想:先假设无冲突,提交时校验,失败重试。
使用清单
- 计数器/标志位/状态机迁移 → atomic 类(注意 getAndAdd 等复合方法已是原子)
- 高竞争计数 → LongAdder/LongAccumulator
- 无锁容器研究/极致性能场景 → 理解 CAS loop 与 ABA 防护后再上手
- 多变量一致性 → 原子引用包不可变快照,或锁
- 业务并发更新 → 数据库 version 乐观锁 + 有限次重试 + 重试耗尽降级
CAS 把”锁”从内核态等待变成了用户态重试,是并发编程从阻塞走向无锁的转折点;记住它的原子性来自硬件、代价是 ABA 与自旋、边界是单变量,就能在 atomic、锁、数据库乐观锁三种形态间自由迁移同一思想。











