CAS是什么
一句话总结:
CAS =
volatile的可见性 + CPU 原子指令的原子性,
让“读 → 改 → 写”三步操作变成一步不可分割的硬件操作,从而实现无锁线程安全。
一、为什么需要 CAS?—— volatile 的局限
回顾这个经典问题:
volatile int count = 0;
count++; // 线程不安全!
❌ 问题在哪?
虽然 count 是 volatile,但 count++ 仍是三步:
- 读:
int tmp = count; - 改:
tmp = tmp + 1; - 写:
count = tmp;
即使每一步都可见,多个线程仍可能同时读到旧值,导致覆盖。
🎯 我们真正想要的是:
“如果当前值是 A,就把它改成 B;否则失败重试。”
这正是 CAS 的语义。
二、CAS 是什么?—— 图解硬件原子指令
🧠 核心思想:
“先检查,再更新”必须是一体的,中间不能被打断。
📈 执行流程图(以 AtomicInteger.incrementAndGet() 为例):
线程A 和 线程B 同时执行 count++
初始:count = 0(volatile,主内存)
线程A: 线程B:
1. 读 count → 0 1. 读 count → 0
2. 计算 0+1 = 1 2. 计算 0+1 = 1
3. CAS(0, 1) 3. CAS(0, 1)
│ │
└──► CPU 原子指令: └──► CPU 原子指令:
"如果当前值==0,就设为1" "如果当前值==0,就设为1"
→ 成功!count=1 → 失败!因为 count 已是 1
线程B 失败后重试:
4. 读 count → 1
5. 计算 1+1 = 2
6. CAS(1, 2) → 成功!count=2
✅ 结果正确:count = 2
🔑 关键:CAS 是 CPU 级别的原子指令(如 x86 的
cmpxchg),
在执行期间,其他 CPU 无法修改该内存地址。
三、Java 如何暴露 CAS?—— 从 Unsafe 到 VarHandle
1. 底层:sun.misc.Unsafe(JDK 8 及以前)
// AtomicInteger 内部
unsafe.compareAndSwapInt(this, valueOffset, expect, update);
2. 现代:java.lang.invoke.VarHandle(JDK 9+)
VarHandle.handle().compareAndSet(obj, expected, new);
3. 封装:java.util.concurrent.atomic 包
AtomicIntegerAtomicReferenceAtomicLongAtomicBoolean- ……
💡 你不需要直接调用
Unsafe,用AtomicXXX就够了。
四、CAS 的三大要素(必须同时满足)
| 要素 | 说明 | Java 实现 |
|---|---|---|
| 1. volatile 字段 | 保证每次读取都是最新值 | AtomicInteger 内部 volatile int value |
| 2. 原子 CAS 指令 | 硬件保证“比较+赋值”不可分割 | Unsafe.compareAndSwapInt() |
| 3. 自旋重试机制 | 失败后循环重试,直到成功 | while (!cas(...)) { } |
🌰 AtomicInteger.incrementAndGet() 伪代码:
public final int incrementAndGet() {
for (;;) {
int current = get(); // volatile 读
int next = current + 1;
if (compareAndSet(current, next)) // CAS
return next;
// 失败?继续循环重试
}
}
五、CAS 的优势 vs synchronized
| 对比项 | synchronized | CAS(AtomicXXX) |
|---|---|---|
| 锁机制 | 悲观锁(阻塞线程) | 乐观锁(无锁,失败重试) |
| 性能 | 高竞争下开销大(线程挂起/唤醒) | 低竞争下极快(纯用户态) |
| 适用场景 | 复杂逻辑、多步骤操作 | 简单状态更新(计数器、标志位) |
| 公平性 | 可能饥饿 | 无公平性,但高吞吐 |
✅ 原则:
- 简单原子操作 → 用
AtomicXXX;- 复杂临界区 → 用
synchronized。
六、CAS 的局限:ABA 问题
❓ 什么是 ABA?
- 线程A 读到值 A;
- 线程B 将 A → B → A;
- 线程A 执行 CAS(A, C) → 成功;
- 但中间状态已变,逻辑可能错误。
🌰 例子:栈顶指针
初始:栈顶 = A
线程1:准备 pop A
线程2:pop A → push B → pop B → push A
线程1:CAS(A, null) → 成功,但栈已被篡改!
✅ 解决方案:带版本号的 CAS
AtomicStampedReferenceAtomicMarkableReference
💡 大多数场景(如计数器)ABA 无影响,无需过度担心。
七、何时使用 CAS?—— 三类典型场景
| 场景 | 示例 | 推荐类 |
|---|---|---|
| 计数器 | QPS 统计、请求计数 | AtomicLong |
| 状态标志切换 | 开关、状态机 | AtomicBoolean / AtomicReference<Enum> |
| 无锁数据结构 | 队列、栈、链表 | ConcurrentLinkedQueue(内部用 CAS) |