并发编程

CAS 与原子操作:原理、ABA 与 LongAdder

从 Compare-And-Set 的硬件语义出发,理解自旋更新、ABA、Java 原子类和 LongAdder 分段计数。

难度:深入更新:2026-07-16

一、CAS 是什么

CAS 是 Compare-And-Set,也常称 Compare-And-Swap。它包含三个操作数:

内存位置 V
期望值 A
新值 B

原子语义是:

如果 V == A:把 V 更新为 B,并返回成功
如果 V != A:不修改 V,并返回失败

比较和写入必须作为不可分割的一次原子操作完成,否则多个线程仍可能在判断与写入之间互相覆盖。

CAS 重试流程与 LongAdder 分段计数结构

二、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));

执行过程是:

  1. 读取当前值;
  2. 基于当前值计算新值;
  3. CAS 尝试更新;
  4. 失败说明有其他线程抢先修改,重新读取并重试。

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 原子类体系

基本类型

  • AtomicInteger
  • AtomicLong
  • AtomicBoolean

适合序列号、单状态标记和低到中等竞争计数。

引用类型

  • AtomicReference
  • AtomicStampedReference
  • AtomicMarkableReference

适合不可变状态整体替换、版本控制和 ABA 治理。

数组类型

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray

它们保证数组元素的原子访问,不代表多个元素组合操作自动原子。

字段更新器

  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater
  • AtomicReferenceFieldUpdater

在不为每个对象额外创建 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 如何选

维度AtomicLongLongAdder
数据结构单个 valuebase + Cell
低竞争简单直接略有额外逻辑
高并发写热点明显分散竞争,吞吐更好
读取单值读取汇总多个槽位
一致性单变量原子读写sum 非严格原子快照
典型用途序列、状态、精确原子值指标和高并发统计

十、高频面试问题

  1. CAS 的三个操作数是什么?
  2. CAS 为什么具有原子性?
  3. CAS 与 volatile 分别解决什么问题?
  4. 高竞争下 CAS 为什么变慢?
  5. 什么是 ABA,什么时候有实际危害?
  6. AtomicStampedReference 如何解决 ABA?
  7. AtomicLong 和 LongAdder 有什么区别?
  8. LongAdder 为什么不能用作严格序列号?
  9. CAS 循环中的函数为什么应避免副作用?
  10. VarHandle 的 acquire/release 与 volatile 模式有什么差别?

参考资料

DISCUSSION

评论与讨论

留下你的想法