Skip to content

Atomic* / CAS / LongAdder ​

在总览中的位置:Java 并发模型总览 §4
前置基础:CAS 与无锁原子基础
相关主题:ConcurrentHashMap(CHM)速览 · synchronized / Lock / volatile

一句话定位 ​

Atomic* 用 CAS 解决单变量原子更新,LongAdder 用“分散热点、最终汇总”解决高冲突计数;它们都很快,但都不等于“复杂业务可以完全不要锁”。

代码索引 ​

主题Lab 说明源码
volatile / AtomicInteger / synchronized 对比volatile-vs-atomic-vs-lockVolatileVsAtomicVsLock.java
并发容器中的原子复合操作与竞态边界map-concurrency-compareMapConcurrencyCompare.java

为什么需要 ​

工程里有一大类问题很典型:

  • “只想把一个计数器 +1”
  • “只想把状态从 IDLE 切到 RUNNING,且只能成功一次”
  • “只想把全局配置引用整体替换掉”

如果为了这种单变量操作也上一把大锁,往往会付出不必要的阻塞和上下文切换成本;这就是 Atomic* 和 CAS 的用武之地。

但另一个极端也很危险:

  • 把多个字段拆成多个 Atomic*,误以为业务整体就自动一致
  • 高并发热点计数还坚持用单个 AtomicLong,导致 CAS 自旋冲突严重
  • 把 LongAdder 用到要求“每次读取都精确一致”的金额/库存场景

所以这类原语最重要的不是 API,而是边界。

底层机制 ​

1. CAS 是什么:比较后再交换 ​

CAS(Compare-And-Set / Compare-And-Swap)可以粗略理解为:

“只有当当前值仍然是我刚才看到的旧值时,我才把它改成新值;否则说明有人抢先修改了,我就重试。”

这里先抓住概念:CAS 不是“无代价成功”,而是“先拿旧值试着更新,失败了就重试”。AtomicInteger.incrementAndGet() 把这套重试细节包起来了,所以代码看上去像一步,但底层仍可能经历多轮竞争。

java
AtomicInteger retryCount = new AtomicInteger(0);
retryCount.incrementAndGet();
  • 可能执行顺序:线程 A 和线程 B 先后读到旧值 0;线程 A 先 CAS 成功把值改成 1;线程 B 的第一次 CAS 失败后重新读取,再把 1 改成 2。
  • 预期现象:多个线程并发自增时,每次成功更新都基于 CAS 完成,不需要显式加锁。
  • 观察重点:表面只是一行 incrementAndGet(),底层却可能发生“读旧值 → CAS 失败 → 重试”的循环;冲突越高,重试越多。

底层思路:

  1. 读取当前值
  2. 在本地算出新值
  3. 通过 CAS 尝试写回
  4. 如果期间别人改过了,就重试

优点:

  • 无锁,低到中等冲突时吞吐很好
  • 不需要线程挂起 / 唤醒

代价:

  • 高冲突下会反复自旋重试,CPU 可能被烧掉
  • 天然更适合单变量,不适合长临界区、多字段事务

2. Atomic* 家族:按数据形态选 ​

AtomicInteger / AtomicLong ​

适合:

  • 点赞数、任务完成数、重试次数、下载进度累计
  • 精确递增序列(更常见是 AtomicLong)
java
AtomicLong requestId = new AtomicLong(1000);
long id = requestId.incrementAndGet();
  • 可能输出:第一次可能拿到 1001,后续线程继续得到严格递增的唯一值。
  • 预期现象:适合发号器这类“单点精确递增”需求;不会像 LongAdder 那样把值分散到多个槽位后再汇总。
  • 观察重点:如果这里是全局热点且写冲突极高,CAS 失败重试会增加 CPU 压力。

AtomicBoolean ​

适合:

  • 一次性启动门闩
  • “只允许一个线程成功开启”
java
AtomicBoolean started = new AtomicBoolean(false);
if (started.compareAndSet(false, true)) {
    startWorker();
}
  • 可能执行顺序:多个线程都可能同时尝试启动,但只有一个线程能把 false 原子切成 true 并真正调用 startWorker()。
  • 预期现象:启动逻辑只会成功一次,其余线程看到 CAS 失败后直接跳过。
  • 观察重点:这类 once-only 门闩很适合 AtomicBoolean;若启动流程还涉及更多共享状态,就要警惕单个原子位不够表达完整约束。

AtomicReference<T> ​

适合:

  • 不可变配置快照整体替换
  • 小型状态机的原子切换
java
AtomicReference<State> state = new AtomicReference<>(State.IDLE);
boolean ok = state.compareAndSet(State.IDLE, State.RUNNING);
  • 可能输出:第一次从 IDLE 切到 RUNNING 时 ok=true;若别的线程已提前切换,再尝试就会得到 false。
  • 预期现象:适合小型状态机的一次性跃迁或引用替换;调用方可以直接根据布尔结果决定后续逻辑。
  • 观察重点:AtomicReference 只保证“引用切换”原子,不保证被引用对象内部字段后续修改也天然安全。

3. LongAdder:为高冲突计数分桶减压 ​

LongAdder 的核心思路不是“一个格子大家抢”,而是:

  • 低冲突时先更新基值
  • 冲突高时把热点拆散到多个 cell
  • 读取时再把多个 cell 汇总起来

这里的概念重点是:LongAdder 不是把所有线程都压到同一个计数点上,而是把热点拆散到多个槽位,各自累加,读的时候再汇总。所以它更像“高并发统计器”,不是“严格顺序计数器”。

java
LongAdder qps = new LongAdder();
qps.increment();
long current = qps.sum();
  • 可能执行顺序:多个线程各自在不同 cell 上执行 increment();主线程需要读取指标时,再通过 sum() 把 base 和多个 cell 的值汇总成当前总数。
  • 预期现象:高并发写入时,多个线程不必长期争抢同一个原子点,吞吐通常比单个 AtomicLong 更稳。
  • 观察重点:sum() 得到的是汇总结果,适合指标统计;它不是严格连续发号器,也不擅长每次读取都要求强一致精确值的业务。

适合:

  • QPS、命中数、埋点计数、限流窗口统计
  • 写多读少、且读到的是“近实时统计值”而不是强一致交易值

不适合:

  • 全局唯一 ID 生成
  • 每次读取都要求强一致精确值的业务余额/库存
  • 需要 compareAndSet 语义的状态切换

4. 什么时候别用 CAS / Atomic ​

场景为什么不适合
多字段必须一起更新多个 Atomic* 之间没有天然事务一致性
临界区里还有 IO、锁、回调CAS 只能保护一个原子点,不保护整段复杂流程
冲突极高且失败重试昂贵自旋会放大 CPU 成本
需要避免 ABA可能需要 AtomicStampedReference / 版本号辅助

5. Atomic vs 锁:常用判断法 ​

  • 只涉及单个共享变量:优先考虑 Atomic*
  • 写冲突极高的纯计数统计:优先考虑 LongAdder
  • 涉及多字段、跨步骤约束:更可能需要锁或不可变状态重构
  • 需要等待条件、超时、取消:这已经不是 Atomic 的问题域

Android / Flutter / Web / Backend 对照 ​

维度Atomic / CAS / LongAdderAndroidFlutterWebBackend
共享模型共享内存原子更新下载计数、状态位、配置引用isolate 默认不共享内存,少直接谈 CASJS 主线程不直接暴露这套原语指标、限流、状态机大量使用
最适合单变量计数/切换点赞数、后台任务门闩、埋点更多通过消息与不可变状态更新更多依靠事件序列QPS、缓存指标、工作流状态
最大误区以为 Atomic 代表整段业务线程安全多个 Atomic 拼起来假装事务把共享内存思维硬搬过去把 Promise 顺序和 CAS 混谈高冲突下单点原子计数成为热点

常见场景 ​

1. Android 下载 / 上传完成数统计 ​

java
AtomicInteger finishedCount = new AtomicInteger(0);

void onOneTaskFinished() {
    int done = finishedCount.incrementAndGet();
    System.out.println("finished=" + done);
}
  • 可能输出:finished=1、finished=2、finished=3 ...,顺序值递增,但打印先后仍可能受线程调度影响。
  • 预期现象:计数结果应精确递增,不会像 volatile int++ 那样丢更新。
  • 观察重点:输出顺序和数字连续性是两回事;看到 finished=3 先于 finished=2 打印,并不代表计数器错了,可能只是打印时序交错。

为什么适合:

  • 单变量累加
  • 需要精确结果
  • 读写频率中等

2. 只允许启动一次的后台任务 ​

java
AtomicBoolean started = new AtomicBoolean(false);

void startIfNeeded() {
    if (started.compareAndSet(false, true)) {
        doStart();
    }
}
  • 可能执行顺序:多个调用方都能进到 startIfNeeded(),但只有 CAS 成功的那一个线程会真正执行 doStart()。
  • 预期现象:可以稳定防止重复初始化 / 重复订阅。
  • 观察重点:如果 doStart() 失败后需要回滚状态,就不能只停留在这个最小示例,还要补失败补偿语义。

适合:

  • SDK 初始化
  • 后台 worker 启动门闩
  • 避免重复订阅 / 重复上报

3. 后端热点指标统计 ​

java
LongAdder successCounter = new LongAdder();

void onSuccess() {
    successCounter.increment();
}
  • 预期现象:高并发成功回调下,计数热点被分散,整体吞吐比单点 CAS 计数更平滑。
  • 观察重点:它更适合做指标,不适合承载“每次 +1 后马上拿全局精确值”的交易型语义。

适合:

  • QPS、成功数、失败数、命中数
  • 写多读少
  • 指标用途重于事务用途

4. 不可变配置快照切换 ​

java
AtomicReference<AppConfig> configRef = new AtomicReference<>(AppConfig.defaultConfig());

void reload(AppConfig fresh) {
    configRef.set(fresh);
}
  • 可能执行顺序:读线程始终读取当前引用;重载线程构造好 fresh 后一次性替换引用;后续读取看到的是整个新快照。
  • 预期现象:比“共享一个可变配置对象并逐字段改”更容易推理,不容易让读线程看到半更新组合。
  • 观察重点:要让这种写法成立,AppConfig 最好不可变;AtomicReference 保护的是引用切换,不是对象内部后续乱改。

前提:

  • AppConfig 尽量是不可变对象
  • 切换的是“整个引用”,而不是一个对象内部被多线程随意改字段

常见坑 ​

坑现象修法
用 volatile 计数,结果偶发偏小丢更新改用 AtomicInteger / AtomicLong
多个 Atomic* 拼装复杂状态局部看都原子,整体却不一致用锁或不可变状态整体替换
热点统计坚持用单个 AtomicLong高并发下 CAS 冲突、自旋高 CPU改用 LongAdder
把 LongAdder 用于发号器/金额读取值不是你以为的那种强一致序列发号器用 AtomicLong,交易值用锁/数据库
忽略 ABAcompareAndSet 看似成功,语义却已变化加版本号或 AtomicStampedReference
自旋失败后没有退避策略高冲突场景 CPU 飙升重构热点、拆分状态或改用锁

与相近概念对比 ​

概念最擅长最不擅长
volatile标志位、可见性读改写原子操作
AtomicInteger / AtomicLong精确单变量计数、CAS 切换高冲突热点统计
AtomicReference<T>不可变对象引用切换对象内部多字段事务一致性
LongAdder高并发写热点统计强一致读取、序列生成
synchronized / Lock多字段、长约束、复杂协同单变量轻量更新

对应实验 ​

Lab说明源码
volatile-vs-atomic-vs-lockvolatile-vs-atomic-vs-lockVolatileVsAtomicVsLock.java
map-concurrency-comparemap-concurrency-compareMapConcurrencyCompare.java

当前仍缺:LongAdder 与 AtomicLong 的热点对比基准、ABA / AtomicStampedReference 演示、AtomicReference 状态机专属实验。

复习检查题 ​

  1. 为什么 AtomicInteger.incrementAndGet() 在低到中等冲突下通常比加锁更轻?

    答:因为它基于 CAS,无需线程阻塞和唤醒,成功时只是在用户态完成一次原子更新,避免了锁带来的上下文切换开销。

  2. 什么情况下 Atomic* 也会成为性能问题?

    答:当多个线程同时疯狂争抢同一个原子变量时,CAS 会不断失败并自旋重试,造成 CPU 消耗上升。这时热点计数可考虑 LongAdder,复杂约束则考虑拆分状态或改用锁。

  3. 为什么 LongAdder 不能直接替代 AtomicLong 做发号器?

    答:因为 LongAdder 的设计目标是高并发统计而不是强一致单点序列。它会把写压力分散到多个 cell,再在读取时汇总,不提供“每次 increment 后马上拿到一个全局唯一、严格连续值”的语义。

  4. AtomicReference<AppConfig> 的正确用法前提是什么?

    答:前提是 AppConfig 最好是不可变对象,或者至少遵守“整体替换、不在共享后继续随意改内部字段”的规则。AtomicReference 只保证引用切换原子,不保证对象内部字段后来被改时的整体一致性。

  5. 什么时候应该从 Atomic 退回到锁?

    答:当状态涉及多个字段必须一起变化、流程跨多个步骤、或者高冲突自旋成本已经超过锁成本时,应该使用锁或重新设计状态模型,而不是硬把复杂业务塞进 Atomic。

速记 ​

  • Atomic 解决单变量,Lock 解决多字段和复杂约束。
  • CAS 快,但高冲突会自旋烧 CPU。
  • AtomicLong 重精确,LongAdder 重吞吐。
  • 多个 Atomic 拼起来,不等于业务事务安全。

站点构建时间:2026/8/24 23:43:17