CAS 与原子操作:原理、ABA 与 LongAdder
从 Compare-And-Set 的硬件语义出发,理解自旋更新、ABA、Java 原子类和 LongAdder 分段计数。
一、CAS 是什么
CAS 是 Compare-And-Set,也常称 Compare-And-Swap。它包含三个操作数:
内存位置 V
期望值 A
新值 B
原子语义是:
如果 V == A:把 V 更新为 B,并返回成功
如果 V != A:不修改 V,并返回失败
比较和写入必须作为不可分割的一次原子操作完成,否则多个线程仍可能在判断与写入之间互相覆盖。
二、Java 如何使用 CAS
Java 中常通过原子类或 VarHandle 使用 CAS:
AtomicInteger count = new AtomicInteger(0);
int oldValue;
int newValue;
do {
oldValue = count.get();
newValue = oldValue + 1;
} while (!count.compareAndSet(oldValue, newValue));
执行过程是:
- 读取当前值;
- 基于当前值计算新值;
- CAS 尝试更新;
- 失败说明有其他线程抢先修改,重新读取并重试。
JVM 会根据处理器架构把原子操作映射为相应的原子指令或指令序列。例如 x86 常使用带锁语义的原子指令,部分 RISC 架构可能使用 Load-Linked/Store-Conditional 一类机制。不要把 CAS 固定等同于某一条 CPU 指令。
三、CAS 保证了什么
原子性
CAS 保证对一个目标变量的“比较并更新”是原子的。
可见性和有序性
Java API 的内存语义由具体方法决定。AtomicInteger.compareAndSet() 具有 VarHandle.compareAndSet() 对应的 volatile 读写语义;getAcquire()、setRelease()、compareAndExchangeAcquire() 等方法则提供不同强度的内存顺序。
因此不能笼统说“任何 CAS 都自动等同于完整 volatile 屏障”,应看调用的具体 API。
不能自动保证复合业务原子性
CAS 只保护参与比较的那个状态。如果业务约束同时依赖多个可独立变化的字段,单字段 CAS 仍可能失败。
可以把多个字段封装进不可变对象,再通过 AtomicReference 整体替换:
record AccountState(long available, long frozen, long version) {}
AtomicReference<AccountState> ref =
new AtomicReference<>(new AccountState(100, 0, 1));
四、CAS 的优点与局限
优点
- 无竞争或低竞争时路径短;
- 不需要线程进入阻塞队列;
- 避免部分挂起、唤醒和上下文切换成本;
- 适合构建原子类、无锁队列和同步器。
高竞争下自旋成本高
大量线程更新同一个变量时,同一时刻只有一个线程成功,其余线程不断重读和重试,会消耗 CPU,并造成缓存行在多个核心之间频繁失效。
常见治理方式:
- 分散热点,例如 LongAdder;
- 随机退避或限制重试次数;
- 极短等待中使用
Thread.onSpinWait()提示处理器; - 竞争持续时间不可控时转为阻塞锁;
- 重新进行数据分片或串行化。
Thread.onSpinWait() 只优化自旋提示,不提供可见性、原子性或锁语义。
不保证公平
CAS 只决定本次竞争谁成功,不维护先来先得。某个线程可能连续失败,理论上存在饥饿风险。
只适合可重试操作
CAS 循环中的计算函数可能被重复执行,不能包含不可重复副作用:
atomic.updateAndGet(old -> {
// 不应在这里扣款、发消息或调用不可重复的远程接口
return old + 1;
});
五、ABA 问题
ABA 指一个值经历:
A → B → A
线程 1 读取 A 后暂停;线程 2 把值改为 B,又改回 A;线程 1 恢复时 CAS 仍能成功,因为它只看到当前值还是 A,却不知道状态曾经变化。
ABA 是否有问题取决于业务语义。简单计数可能不关心历史变化,但链表节点复用、资源状态和版本控制可能非常敏感。
解决方案
版本号
把值与版本作为整体比较:
(A, version=1)
→ (B, version=2)
→ (A, version=3)
虽然值回到 A,版本已经不同。
AtomicStampedReference
同时维护引用和整数 stamp,CAS 时一起比较。
AtomicMarkableReference
同时维护引用和布尔标记,适合只关心“是否被删除/修改过”之类的二态信息。
六、Java 原子类体系
基本类型
AtomicIntegerAtomicLongAtomicBoolean
适合序列号、单状态标记和低到中等竞争计数。
引用类型
AtomicReferenceAtomicStampedReferenceAtomicMarkableReference
适合不可变状态整体替换、版本控制和 ABA 治理。
数组类型
AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray
它们保证数组元素的原子访问,不代表多个元素组合操作自动原子。
字段更新器
AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicReferenceFieldUpdater
在不为每个对象额外创建 Atomic 包装对象的情况下,对指定 volatile 字段做原子更新。它们带有反射式访问约束和类型限制。
VarHandle
VarHandle 可以操作字段、数组元素和某些堆外结构,提供 plain、opaque、acquire/release、volatile 和原子更新等多种访问模式。它是现代 JDK 中表达底层内存访问语义的重要 API。
七、AtomicLong 为什么在高竞争下变慢
AtomicLong 只有一个热点 value:
线程 A ─┐
线程 B ─┼→ AtomicLong.value
线程 C ─┤
线程 D ─┘
并发越高,CAS 失败、缓存一致性通信和自旋越多。低竞争时它非常直接,高竞争写入时则可能成为热点。
八、LongAdder 如何分散竞争
LongAdder 的核心思想是:
base + Cell[]
低竞争时优先 CAS 更新 base;出现竞争后,把不同线程分散到不同 Cell。最终:
sum = base + Σ Cell.value
如果某个 Cell 仍然冲突,线程会重新计算探针并尝试其他槽位;必要时扩容 Cell 数组。
空间换时间
LongAdder 使用更多内存换取更低热点竞争。Cell 通常还会采取缓存行填充等手段,降低不同 Cell 之间的伪共享。
sum 不是严格原子快照
sum() 汇总期间,其他线程仍可更新不同 Cell,因此结果不代表一个全局锁保护下的绝对瞬时值。
LongAdder 适合:
- QPS、调用次数、命中次数;
- 监控指标;
- 高并发统计。
不适合:
- 唯一序列号;
- 账户余额、精确库存;
- 必须基于当前精确值继续决策的业务。
九、AtomicLong 与 LongAdder 如何选
| 维度 | AtomicLong | LongAdder |
|---|---|---|
| 数据结构 | 单个 value | base + Cell |
| 低竞争 | 简单直接 | 略有额外逻辑 |
| 高并发写 | 热点明显 | 分散竞争,吞吐更好 |
| 读取 | 单值读取 | 汇总多个槽位 |
| 一致性 | 单变量原子读写 | sum 非严格原子快照 |
| 典型用途 | 序列、状态、精确原子值 | 指标和高并发统计 |
十、高频面试问题
- CAS 的三个操作数是什么?
- CAS 为什么具有原子性?
- CAS 与 volatile 分别解决什么问题?
- 高竞争下 CAS 为什么变慢?
- 什么是 ABA,什么时候有实际危害?
- AtomicStampedReference 如何解决 ABA?
- AtomicLong 和 LongAdder 有什么区别?
- LongAdder 为什么不能用作严格序列号?
- CAS 循环中的函数为什么应避免副作用?
- VarHandle 的 acquire/release 与 volatile 模式有什么差别?
评论与讨论
回复 :
留下你的想法