CAS 与原子类
CAS(Compare And Swap)是 Java 并发的底层基石——synchronized 的锁升级、AQS 的状态修改、原子类的无锁操作,全部依赖 CAS。理解 CAS,就理解了 Java 无锁并发的核心。
CAS 是什么
CAS 是一条 CPU 原子指令(如 x86 的 CMPXCHG),它的语义是:
CAS(内存地址, 期望值, 新值)
if (内存地址的当前值 == 期望值) {
将内存地址的值更新为新值
return true // 更新成功
} else {
return false // 更新失败,说明有其他线程修改了
}
整个操作是原子的——CPU 保证读取-比较-写入不可被中断。
CAS 的自旋模式
当 CAS 失败时,通常会自旋重试——重新读取当前值,重新计算,再尝试 CAS:
// 伪代码:原子递增
int oldValue;
int newValue;
do {
oldValue = currentValue; // 读取当前值
newValue = oldValue + 1; // 计算新值
} while (!CAS(addr, oldValue, newValue)); // 尝试更新,失败则重试
CAS vs 锁
| CAS(乐观锁) | synchronized(悲观锁) | |
|---|---|---|
| 思路 | 假设没冲突,失败重试 | 假设有冲突,先加锁 |
| 线程阻塞 | 不阻塞(自旋) | 可能阻塞 |
| 适用场景 | 竞争不激烈 | 竞争激烈 |
| 开销 | 自旋消耗 CPU | 线程切换消耗 |
低竞争时 CAS 更快(避免了锁的开销),高竞争时锁更优(CAS 大量自旋浪费 CPU)。
Unsafe 类
sun.misc.Unsafe 是 CAS 操作的入口。它提供了直接操作内存、线程调度等底层能力,是 JDK 内部的"后门"。
// Unsafe 中的 CAS 方法
public final native boolean compareAndSwapInt(
Object o, // 目标对象
long offset, // 字段的内存偏移量
int expected, // 期望值
int x // 新值
);
普通开发者不应该直接使用 Unsafe——直接用 JDK 提供的原子类即可。
原子类
java.util.concurrent.atomic 包提供了一系列原子类,底层全部基于 CAS:
基本类型原子类
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // ++count(原子操作)
count.getAndIncrement(); // count++
count.addAndGet(5); // count += 5
count.compareAndSet(5, 10); // CAS: 如果是 5 则改为 10
count.updateAndGet(x -> x * 2); // 自定义更新函数(JDK 8+)
AtomicLong 和 AtomicBoolean 用法类似。
引用类型原子类
AtomicReference<User> ref = new AtomicReference<>(new User("Tom"));
User oldUser = ref.get();
User newUser = new User("Jerry");
ref.compareAndSet(oldUser, newUser); // CAS 更新引用
数组原子类
AtomicIntegerArray arr = new AtomicIntegerArray(10);
arr.getAndIncrement(0); // 原子地递增 arr[0]
arr.compareAndSet(0, 1, 2); // 原子地把 arr[0] 从 1 改为 2
字段更新器
对已有类的 volatile 字段进行原子更新(无需改原类):
public class Account {
volatile int balance; // 必须是 volatile
}
AtomicIntegerFieldUpdater<Account> updater =
AtomicIntegerFieldUpdater.newUpdater(Account.class, "balance");
Account account = new Account();
updater.addAndGet(account, 100); // 原子地 account.balance += 100
ABA 问题
CAS 的经典缺陷:
线程 1 读取值 = A
线程 2 将值 A → B → A
线程 1 CAS(A, newValue) 成功 // 线程 1 认为值没变过,但实际上变过
类比:你去 ATM 取钱,余额 100 元。在你操作期间,有人先取走了 100 又存回了 100。余额看起来没变,但实际发生了两笔交易。
ABA 在什么场景下是真正的问题?
对于简单的计数器(如 AtomicInteger),ABA 通常不是问题——A 和 A 确实是相同的值。
但在以下场景中 ABA 是致命的:
- CAS 操作链表/栈的头指针:头节点被删又重新插入,CAS 认为头没变,但底层结构已经变了
- 复用对象池:对象被回收又重新分配,引用相同但内容已变
解决方案:AtomicStampedReference
给值附加一个版本号(stamp),CAS 时同时比较值和版本号:
AtomicStampedReference<String> ref =
new AtomicStampedReference<>("A", 0); // 初始值 "A",版本 0
// 获取当前值和版本
String value = ref.getReference();
int stamp = ref.getStamp();
// CAS 时同时检查值和版本
boolean success = ref.compareAndSet(
value, "B", // 期望值, 新值
stamp, stamp + 1 // 期望版本, 新版本
);
如果只关心是否修改过(不关心修改了几次),可以用 AtomicMarkableReference:
AtomicMarkableReference<String> ref =
new AtomicMarkableReference<>("A", false);
ref.compareAndSet("A", "B", false, true); // 标记为已修改
LongAdder(JDK 8+)
AtomicLong 在高竞争场景下性能不佳——大量线程 CAS 同一个变量,失败率很高,自旋浪费 CPU。
LongAdder 通过分散热点解决这个问题:
AtomicLong(所有线程竞争一个值):
线程1 ──┐
线程2 ──┤──► [value] ← CAS 竞争激烈
线程3 ──┘
LongAdder(分散竞争到多个 Cell):
线程1 ──► [Cell 0]
线程2 ──► [Cell 1] 最终 sum = base + Cell[0] + Cell[1] + ...
线程3 ──► [Cell 2]
原理
// LongAdder 内部(继承自 Striped64)
volatile long base; // 基础值,竞争不激烈时使用
volatile Cell[] cells; // 分散的 Cell 数组
// add 逻辑
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
if ((as = cells) != null || // 如果 cells 已初始化
!casBase(b = base, b + x)) { // 或者 base 的 CAS 失败
// 竞争激烈,使用 Cell
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null || // 线程映射到某个 Cell
!(uncontended = a.cas(v = a.value, v + x))) {
longAccumulate(x, null, uncontended); // Cell 也 CAS 失败,扩容
}
}
}
// sum(非精确,不加锁)
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
核心思想:
- 无竞争时,直接 CAS 修改
base - 有竞争时,将线程分散到不同的
Cell,减少冲突 sum()时将base和所有Cell的值累加
AtomicLong vs LongAdder
| AtomicLong | LongAdder | |
|---|---|---|
| 精确性 | get() 精确 |
sum() 不精确(非原子) |
| 低竞争性能 | 好 | 一样好 |
| 高竞争性能 | 差(大量自旋) | 很好(分散热点) |
| 内存 | 固定 | Cell 数组,较大 |
| 适用场景 | 需要精确值 | 统计计数(允许非精确 sum) |
LongAdder 是高并发计数器的首选。LongAccumulator 是其泛化版本,支持自定义累加函数。
ConcurrentHashMap在 JDK 8 中的size()实现就使用了类似LongAdder的CounterCell分散计数。
CAS 操作的三大问题总结
| 问题 | 描述 | 解决方案 |
|---|---|---|
| ABA 问题 | 值被改回原值,CAS 无法感知 | AtomicStampedReference(版本号) |
| 自旋开销 | 高竞争时大量空转浪费 CPU | LongAdder(分散热点)或改用锁 |
| 只能保证一个变量的原子性 | 多个变量无法同时 CAS | 合并为一个对象用 AtomicReference,或用锁 |
生产环境核心踩坑点
| 问题 | 答案要点 |
|---|---|
| CAS 是什么?底层实现? | Compare And Swap,CPU 原子指令(CMPXCHG) |
| CAS 有什么问题? | ABA、自旋开销、单变量原子性 |
| ABA 问题如何解决? | AtomicStampedReference 加版本号 |
| AtomicLong 和 LongAdder 有什么区别? | LongAdder 分散热点到 Cell 数组,高竞争性能更好 |
| LongAdder 的原理? | base + Cell[] 分段计数,sum 时汇总 |