CAS从CPU原子指令到Java原子类完整原理
CAS 是 Compare-And-Set/Swap,比较并设置(交换)。调用方提供期望旧值和新值:只有共享位置当前值仍等于期望值时,才原子地写入新值;否则不修改并返回失败。
它解决的不是“线程永远不冲突”,而是把“检查旧状态仍成立”和“提交新状态”合成一个不可分割的条件更新。Java 7/8 的 Atomic 类、AQS状态更新、ConcurrentHashMap部分操作等都依赖CAS。
学习目标
- 从
count++丢失更新理解CAS存在原因。 - 讲清内存值V、期望值E、新值N和返回结果。
- 理解CPU原子指令/独占访问是实现基础,但各架构不同。
- 理解JDK7/8中Atomic类、volatile字段和
Unsafe的关系。 - 解释CAS循环怎样从失败中重新计算,而非盲写旧结果。
- 区分值比较、引用身份比较和业务相等。
- 解释CAS的可见性/有序性与JMM关系。
- 识别ABA、竞争自旋、缓存行抖动、活锁和饥饿。
- 区分AtomicLong和LongAdder的正确场景。
- 使用AtomicReference整体替换多字段不可变状态。
- 理解lock-free不等于wait-free,也不等于一定更快。
一、为什么count++会丢失更新
count++;逻辑上包含:
read count
calculate count + 1
write countflowchart TD
A["线程A读到0"] --> C["A计算1"]
B["线程B读到0"] --> D["B计算1"]
C --> E["A写1"]
D --> F["B写1"]
E --> G["最终1而不是2"]
F --> G即使 count 是 volatile,每次读写都可见,也不能阻止两个线程在读和写之间交错。CAS把最后提交改成条件提交:只有当前值仍是我读取的旧值,才允许写入。
二、CAS的三个输入和一个结果
V:共享位置当前值
E:调用方期望的旧值 expected
N:准备写入的新值 newValue
result:是否更新成功语义:
原子地执行:
if V == E:
V = N
return true
else:
return false“原子地”表示其他线程不能观察到比较完成但写入尚未提交的中间窗口。
三、完整CAS循环
flowchart TD
A["读取old"] --> B["根据old计算next"]
B --> C{"CAS old→next成功吗"}
C -- "成功" --> D["返回next"]
C -- "失败" --> E["说明状态已变化"]
E --> F["重新读取最新值"]
F --> B伪代码:
for (;;) {
int oldValue = value.get();
int newValue = oldValue + 1;
if (value.compareAndSet(oldValue, newValue)) {
return newValue;
}
}失败后必须基于最新状态重新计算。若更新函数有发送消息、扣款等副作用,CAS重试可能重复执行副作用,因此传给 updateAndGet 一类API的函数必须是无副作用或可安全重复计算的纯函数。
四、CPU怎样保证原子比较更新
不同架构实现不同:
- x86常使用带锁语义的比较交换指令,保证目标内存位置的原子读改写并建立所需内存顺序。
- ARM等架构可使用独占加载/独占存储或原子指令;若独占状态被其他写破坏,存储失败并重试。
- JVM根据CPU架构、字段类型和内存语义选择机器指令与屏障。
不能把CAS永久理解成某一条固定x86指令。Java承诺的是Atomic API和JMM语义,底层实现随架构、JVM和版本变化。
原子指令也不是免费:高竞争时多个核心反复争夺同一缓存行,缓存所有权在核心之间迁移,CAS失败、流水线和互联流量都增加。
五、JDK7/8中AtomicInteger怎样连接到CPU
以常见HotSpot实现理解:
flowchart TD
A["AtomicInteger.incrementAndGet"] --> B["volatile value字段"]
B --> C["Unsafe原子读改写/比较更新"]
C --> D["JVM intrinsic或本地实现"]
D --> E["目标CPU原子指令与屏障"]JDK 7/8 的 Atomic 类内部常借助 sun.misc.Unsafe 对字段内存偏移执行原子操作。Unsafe 是内部实现能力,业务代码不应直接依赖其非标准API。Java 9以后提供VarHandle等标准化能力,但这不改变学习JDK7/8 Atomic类的重要性。
不同JDK源码可能把循环写在Java层,或调用 getAndAddInt 等由JVM优化的原子原语。面试应讲语义和版本边界,不死背某一小版本源码行号。
六、volatile和CAS各负责什么
典型Atomic字段具有volatile语义:
get()能读取具有相应可见性的值。- 成功CAS既完成原子条件更新,也发布更新前的内存效果。
- 失败CAS至少告诉调用者期望值已不成立,具体API内存效果应以Javadoc为准。
CAS解决条件写入的原子性,volatile/JMM语义解决相关可见性和顺序。只用volatile不能合并读改写,只用一个没有正确内存语义的硬件比较也不能自动满足Java跨线程承诺。
详细JMM见 JMM重排序与安全发布。
七、比较的到底是什么
基本类型
比较相应值的位模式/数值表示,调用者应通过Atomic API使用,不自行假设特殊浮点语义。
AtomicReference
CAS比较的是引用身份是否仍为同一个对象,不调用业务对象的 equals()。
State oldState = state.get();
State equalButDifferentObject = new State(oldState.getValue());即使两个对象 equals() 为true,只要不是同一引用,compareAndSet(equalButDifferentObject, next) 也不会把它当作当前期望引用。
这正适合“我基于哪一版快照计算”的并发协议;业务等价与并发版本身份不能混为一谈。
八、JDK7/8可运行Demo:AtomicInteger
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounterDemo {
public static void main(String[] args) throws Exception {
final AtomicInteger counter = new AtomicInteger(0);
List<Thread> threads = new ArrayList<Thread>();
for (int i = 0; i < 4; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
counter.incrementAndGet();
}
}
});
threads.add(thread);
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println(counter.get());
}
}运行:
javac AtomicCounterDemo.java
java AtomicCounterDemo结果应为 40000。使用匿名内部类,兼容JDK7/8。
九、多个字段怎样用AtomicReference整体替换
两个Atomic变量无法天然保证联合不变量。例如库存和版本必须一起变化:
final class InventoryState {
private final int stock;
private final long version;
InventoryState(int stock, long version) {
this.stock = stock;
this.version = version;
}
int getStock() { return stock; }
long getVersion() { return version; }
}AtomicReference<InventoryState> state =
new AtomicReference<InventoryState>(new InventoryState(10, 1));
for (;;) {
InventoryState oldState = state.get();
if (oldState.getStock() <= 0) {
throw new IllegalStateException("库存不足");
}
InventoryState next = new InventoryState(
oldState.getStock() - 1,
oldState.getVersion() + 1
);
if (state.compareAndSet(oldState, next)) {
break;
}
}读线程总能得到某个完整不可变快照。但如果更新包含数据库扣减、远程调用、消息发送,应使用业务事务/幂等,而不是把副作用放入CAS重试循环。
十、ABA为什么CAS可能被骗
线程T1读取引用A;T2把A改为B再改回同一个A;T1只比较当前引用,仍会成功。若算法关心“期间是否变化过”,单值CAS无法表达历史。
flowchart TD
A["T1读取A"] --> B["T1暂停"]
B --> C["T2执行A→B"]
C --> D["T2执行B→A"]
D --> E["T1比较仍是A"]
E --> F["CAS成功但忽略中间历史"]完整内容见 ABA与版本戳。
十一、竞争为什么让CAS变慢
低竞争时,CAS成功路径短,避免挂起/唤醒。高竞争时:
多个线程读取同一old
→ 只有一个成功
→ 其他全部失败
→ 重新读取计算
→ 再次争同一缓存行问题包括:
- CPU用于失败重试,业务没有前进。
- 热缓存行在核心间迁移。
- 某些线程长期失败,产生饥饿。
- 重试策略一致时可能同步碰撞,类似活锁。
- 高负载下尾延迟明显上升。
优化:降低共享热点、分片、退避、限制重试、批量合并;高竞争复杂临界区直接使用锁/队列可能更稳定。
十二、AtomicLong和LongAdder怎样选择
AtomicLong
- 单一值,
get()是明确当前原子值。 - 支持条件CAS和精确序号。
- 高竞争下所有线程争一个位置。
LongAdder(JDK8)
- 使用Base和多个Cell分散更新热点。
- 高并发统计吞吐通常更好。
sum()汇总多个Cell时,其他线程可能继续更新,不是用于强一致条件判断的原子快照。- 不适合订单序号、余额和“达到阈值只执行一次”。
适合监控计数、请求次数等统计;需要线性化单值或CAS条件更新时用AtomicLong。
十三、lock-free到底是什么意思
- Blocking:线程可能因锁挂起。
- Lock-free:系统整体保证有线程持续取得进展,但某个线程可能长期失败。
- Wait-free:每个线程在有限步骤内完成,保证更强也更难实现。
“没有写 synchronized”不自动证明算法lock-free;使用CAS也不自动wait-free。还要考虑内存回收、队列算法、重试上界和实现证明。
Java GC减少了手工释放内存导致的悬空指针问题,但不消除逻辑ABA、状态重用和外部资源生命周期问题。
十四、CAS和锁如何选
flowchart TD
A["需要并发更新"] --> B{"单值且计算短吗"}
B -- "否" --> C["优先锁或串行队列"]
B -- "是" --> D{"冲突低吗"}
D -- "是" --> E["Atomic/CAS"]
D -- "否" --> F{"只是统计累加吗"}
F -- "是" --> G["JDK8 LongAdder"]
F -- "否" --> C锁的阻塞/唤醒有成本,但高竞争时能减少无效自旋,并更容易表达多个字段、异常和I/O边界。CAS适合短、纯计算、低冲突的状态转换。不要以“无锁一定快”作为设计依据,要用目标负载看吞吐、CPU和P99。
十五、常见错误
| 错误 | 后果 | 正确方式 |
|---|---|---|
| volatile count++就安全 | 丢失更新 | Atomic或锁 |
| CAS失败仍提交旧next | 覆盖新状态 | 重新读取并计算 |
| 更新函数内发消息 | 重试导致重复消息 | 纯函数+提交后副作用/幂等 |
| AtomicReference用equals理解 | CAS比较引用身份 | 使用读取到的原引用 |
| LongAdder做余额 | sum非强一致快照 | AtomicLong/锁/事务 |
| 无限重试 | CPU高、饥饿、P99差 | 退避、上限、降级为锁 |
| 多个Atomic保证多字段事务 | 中间组合不一致 | 不可变快照整体CAS或锁 |
| CAS无死锁所以一定更好 | 仍可活锁/饥饿 | 按进度与竞争选择 |
十六、线上排查CAS热点
现象:CPU高但吞吐不升、Atomic更新热点、P99上升。
- 用CPU Profile/火焰图定位Atomic、Unsafe、重试循环。
- 查看线程数和同一共享变量的调用来源。
- 比较业务成功次数与CAS尝试次数;源码未暴露时可用基准或埋点复现。
- 检查伪共享:不同热点字段是否落在同一缓存行。
- 检查更新函数是否昂贵或含副作用。
- 按场景尝试分片、LongAdder、批量、退避或锁。
- 在相同正确性和SLO下压测,不只比较平均吞吐。
十七、面试标准回答
CAS是什么
CAS接收当前共享位置、期望旧值和新值,只有当前值仍等于期望时才原子写入并返回成功,否则失败。Java 7/8 Atomic类通常通过volatile字段、Unsafe/JVM原子原语映射到目标CPU指令,失败后基于最新状态重新计算。适合短小低冲突更新,但有ABA、自旋开销、饥饿和单状态限制。
AtomicInteger为什么线程安全
value具有volatile可见性,自增通过原子读改写或CAS循环提交。多个线程读取同一旧值时只有一个能成功,失败者重新读取并计算,因此不会像普通count++那样丢失更新。
CAS一定比锁快吗
不一定。低竞争短操作CAS避免线程阻塞,通常高效;高竞争时失败重试和缓存行迁移消耗CPU,可能产生饥饿和高尾延迟。复杂多字段临界区、I/O或高冲突使用锁更清晰稳定。
十八、关联知识点
十九、学习验收
- [ ] 能画出count++丢失更新和CAS重试。
- [ ] 能解释V、E、N和返回值。
- [ ] 能说明CPU实现随架构变化。
- [ ] 能讲清JDK7/8 Atomic、Unsafe和volatile关系。
- [ ] 能解释AtomicReference比较引用身份。
- [ ] 能写JDK7/8 AtomicInteger Demo。
- [ ] 能用不可变State整体CAS多个字段。
- [ ] 能解释ABA何时有害。
- [ ] 能解释竞争、缓存行迁移、饥饿和活锁。
- [ ] 能正确选择AtomicLong、LongAdder和锁。
本章小结
CAS把“前提仍成立”和“提交新状态”合成原子条件更新。Atomic类再用JMM可见性把它变成可用的并发协议。真正掌握CAS不只是会说比较交换,而是知道失败后重算、引用身份、ABA、缓存行竞争、进度保证和副作用边界,并能在CAS、LongAdder和锁之间按正确性与负载选择。
