Appearance
CAS 与无锁原子基础
一句话定义
CAS(Compare-And-Swap) 是 CPU 提供的原子原语:「只有当内存里的值仍是我看到的旧值时,才写成新值」;是 Atomic*、很多无锁结构的基石,属于乐观并发思路。
可运行对比实验:volatile-vs-atomic-vs-lock · VolatileVsAtomicVsLock.java
为什么要学
读 volatile、AtomicInteger、ConcurrentHashMap 之前,需要分清:
| 概念 | 解决什么 |
|---|---|
| volatile | 可见性 + 部分有序性;不保证 count++ |
| CAS / Atomic* | 单变量的原子读-改-写 |
| synchronized / Lock | 互斥 + 多步复合逻辑 |
CAS 在做什么(口语)
比喻:白板上的数字是 10。你和小王都要改成 11:
- 你先读:旧值 = 10
- 你算:新值 = 11
- 你去换:仅当白板上仍是 10 才写成 11;若已被别人改成 11,你失败重试
这就是 compareAndSet(expected, update) 的心智模型。
java
// AtomicInteger 内部语义(简化)
boolean compareAndSet(int expect, int update) {
// 硬件保证:读-比较-写 这一步原子
if (current == expect) { current = update; return true; }
return false;
}CPU 层到底做了什么(为什么"读-比较-写"能原子)
普通的三步指令(读 → 比较 → 写)在 CPU 上会被其他核的写入从中间打断;CAS 之所以原子,靠的是硬件指令,而不是 Java 代码:
- x86:
LOCK CMPXCHG——LOCK前缀让指令执行期间锁住缓存行(Cache Line)/ 总线,其他核的读写被阻塞到指令完成。 - ARM:
LDXR / STXR(Load-Exclusive / Store-Exclusive)—— 读标记独占,写时若发现该地址已被其他核改过则失败返回,由软件重试。 - 依赖的底层机制是 缓存一致性协议(MESI) :保证一个核写回后,其他核能观察到最新值,
LOCK再把"检查 + 写入"合并成一个不可分割的步骤。
text
线程 A 执行 compareAndSet(10, 11)
│
├─ 1. 读内存: current == 10 ? ┐
├─ 2. 比较: 10 == 10 ? 是 │ LOCK CMPXCHG 期间
├─ 3. 写入: 内存 ← 11 ✅ │ 总线/缓存行被锁定,
└─ 4. 返回 true │ 线程 B 的写入必须等待自旋重试(Spin Retry):失败不是异常,重试是常态
CAS 失败只表示"有人抢先改了值",正确的姿势是读新值 → 再算 → 再 CAS,直到成功:
java
// 手写自旋版:等价于 incrementAndGet() 内部行为
AtomicInteger counter = new AtomicInteger(0);
int increment() {
int prev, next;
do {
prev = counter.get(); // 1. 读当前值
next = prev + 1; // 2. 计算新值
} while (!counter.compareAndSet(prev, next)); // 3. 失败就带着新 prev 重试
return next;
}- 可能执行顺序:线程 T1 先
compareAndSet(0, 1)成功;线程 T2 同时带着旧值0重试 → 比较失败 → 重新读得1→ 再 CAS(1, 2)成功。 - 预期现象:所有并发线程最终都把计数 +1,且不会丢更新(这正是
count++做不到的)。 - 观察重点:高冲突时循环可能空转多次(自旋烧 CPU),所以
LongAdder用分段 Cell 分散写热点来减少冲突概率。
优劣
| 优势 | 劣势 |
|---|---|
| 无锁(通常不阻塞线程) | 高冲突时自旋重试,烧 CPU |
| 低冲突下延迟小 | 只适合单变量或能封装成原子状态的问题 |
| 表达「乐观更新」清晰 | ABA 等语义问题要额外处理 |
| 适合计数、状态位、引用切换 | 多字段一致性仍要锁或重新设计 |
乐观锁 vs 悲观锁(概念层)
| 乐观(CAS / Atomic*) | 悲观(synchronized / Lock) | |
|---|---|---|
| 心态 | 先假设不会冲突,冲突再重试 | 先占坑再干活 |
| Java 代表 | AtomicInteger、LongAdder | synchronized、ReentrantLock |
| 冲突低 | 通常更快 | 略重 |
| 冲突高 | 自旋可能更差 | 排队阻塞可能更稳 |
本文只建立概念;代码对比见 volatile lab 场景 2。
Java 里常见 API
| API | 用途 |
|---|---|
AtomicInteger / AtomicLong | 计数、序号 |
AtomicReference<V> | 引用整体切换(配置快照) |
AtomicStampedReference | 带版本号,缓解 ABA |
LongAdder | 极高并发计数,空间换冲突分散 |
VarHandle(JDK 9+) | 更底层的 CAS 入口 |
incrementAndGet() 等在底层就是 CAS 循环。
ABA 问题(知道即可)
线程 A 读到值 V1 → 被 B 改成 V2 又改回 V1 → A 的 CAS 仍成功,但中间状态已变。
引用类型若不能容忍「对象已被换掉又换回来」,用 AtomicStampedReference 或加版本号。
Android / 工程常见用途
| 场景 | 倾向 |
|---|---|
| 点赞数、下载计数 | AtomicInteger / LongAdder |
| 一次性开关、熔断 | AtomicBoolean |
| 配置引用整体替换 | AtomicReference + 不可变对象 |
| UI 状态多字段联动 | 不用 Atomic 硬凑,用不可变 state + StateFlow 或锁 |
ConcurrentHashMap 桶头 CAS | 集合底层;见 map lab |
和后续专题的关系
图注补充:volatile-vs-atomic-vs-lock lab。
- volatile 适用/不适用 → volatile-vs-atomic-vs-lock
- 总览 → Java 并发模型总览 §3、§4
复习检查题
CAS 保证的是什么?不保证什么?
答:保证单次「读旧值 → 比较 → 条件成立才写入」在硬件上是原子的,适合单变量读-改-写(如
incrementAndGet)。不保证多字段一致性、长临界区业务逻辑,也不自动解决 ABA;高冲突时只保证正确性,不保证性能(会自旋烧 CPU)。为什么
volatile不能替代AtomicInteger做count++?答:
count++是读、加、写三步,volatile只保证每次读/写可见,不保证三步整体互斥。多线程仍会丢更新。AtomicInteger用 CAS 把「比较旧值再写入」合成一步原子语义。高并发计数何时考虑
LongAdder而不是AtomicLong?答:写冲突极高、热点集中在少数计数器时,
AtomicLong会大量 CAS 重试。LongAdder用分段 Cell 分散写压力,读时求和,以空间换吞吐。读多写少、需要强一致实时读值时仍可能用AtomicLong或锁。什么是 ABA?什么场景需要关心?
答:线程 A 读到值 V1,期间被改成 V2 又改回 V1,A 的 CAS 仍成功,但中间状态已变。纯数值计数通常无所谓;引用/链表节点等语义下,「对象已被替换又换回来」可能出错,需
AtomicStampedReference或版本号。
速记
- CAS = 比较旧值再更新,失败就重试。
- Atomic* = 乐观、单变量;锁 = 悲观、复合逻辑。
- 冲突低用 CAS 爽,冲突高要警惕自旋。