Appearance
Atomic* / CAS / LongAdder
在总览中的位置:Java 并发模型总览 §4
前置基础:CAS 与无锁原子基础
相关主题:ConcurrentHashMap(CHM)速览 · synchronized / Lock / volatile
一句话定位
Atomic* 用 CAS 解决单变量原子更新,LongAdder 用“分散热点、最终汇总”解决高冲突计数;它们都很快,但都不等于“复杂业务可以完全不要锁”。
代码索引
| 主题 | Lab 说明 | 源码 |
|---|---|---|
volatile / AtomicInteger / synchronized 对比 | volatile-vs-atomic-vs-lock | VolatileVsAtomicVsLock.java |
| 并发容器中的原子复合操作与竞态边界 | map-concurrency-compare | MapConcurrencyCompare.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 失败 → 重试”的循环;冲突越高,重试越多。
底层思路:
- 读取当前值
- 在本地算出新值
- 通过 CAS 尝试写回
- 如果期间别人改过了,就重试
优点:
- 无锁,低到中等冲突时吞吐很好
- 不需要线程挂起 / 唤醒
代价:
- 高冲突下会反复自旋重试,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 / LongAdder | Android | Flutter | Web | Backend |
|---|---|---|---|---|---|
| 共享模型 | 共享内存原子更新 | 下载计数、状态位、配置引用 | isolate 默认不共享内存,少直接谈 CAS | JS 主线程不直接暴露这套原语 | 指标、限流、状态机大量使用 |
| 最适合 | 单变量计数/切换 | 点赞数、后台任务门闩、埋点 | 更多通过消息与不可变状态更新 | 更多依靠事件序列 | 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,交易值用锁/数据库 |
| 忽略 ABA | compareAndSet 看似成功,语义却已变化 | 加版本号或 AtomicStampedReference |
| 自旋失败后没有退避策略 | 高冲突场景 CPU 飙升 | 重构热点、拆分状态或改用锁 |
与相近概念对比
| 概念 | 最擅长 | 最不擅长 |
|---|---|---|
volatile | 标志位、可见性 | 读改写原子操作 |
AtomicInteger / AtomicLong | 精确单变量计数、CAS 切换 | 高冲突热点统计 |
AtomicReference<T> | 不可变对象引用切换 | 对象内部多字段事务一致性 |
LongAdder | 高并发写热点统计 | 强一致读取、序列生成 |
synchronized / Lock | 多字段、长约束、复杂协同 | 单变量轻量更新 |
对应实验
| Lab | 说明 | 源码 |
|---|---|---|
| volatile-vs-atomic-vs-lock | volatile-vs-atomic-vs-lock | VolatileVsAtomicVsLock.java |
| map-concurrency-compare | map-concurrency-compare | MapConcurrencyCompare.java |
当前仍缺:
LongAdder与AtomicLong的热点对比基准、ABA /AtomicStampedReference演示、AtomicReference 状态机专属实验。
复习检查题
为什么
AtomicInteger.incrementAndGet()在低到中等冲突下通常比加锁更轻?答:因为它基于 CAS,无需线程阻塞和唤醒,成功时只是在用户态完成一次原子更新,避免了锁带来的上下文切换开销。
什么情况下
Atomic*也会成为性能问题?答:当多个线程同时疯狂争抢同一个原子变量时,CAS 会不断失败并自旋重试,造成 CPU 消耗上升。这时热点计数可考虑
LongAdder,复杂约束则考虑拆分状态或改用锁。为什么
LongAdder不能直接替代AtomicLong做发号器?答:因为
LongAdder的设计目标是高并发统计而不是强一致单点序列。它会把写压力分散到多个 cell,再在读取时汇总,不提供“每次increment后马上拿到一个全局唯一、严格连续值”的语义。AtomicReference<AppConfig>的正确用法前提是什么?答:前提是
AppConfig最好是不可变对象,或者至少遵守“整体替换、不在共享后继续随意改内部字段”的规则。AtomicReference 只保证引用切换原子,不保证对象内部字段后来被改时的整体一致性。什么时候应该从 Atomic 退回到锁?
答:当状态涉及多个字段必须一起变化、流程跨多个步骤、或者高冲突自旋成本已经超过锁成本时,应该使用锁或重新设计状态模型,而不是硬把复杂业务塞进 Atomic。
速记
- Atomic 解决单变量,Lock 解决多字段和复杂约束。
- CAS 快,但高冲突会自旋烧 CPU。
AtomicLong重精确,LongAdder重吞吐。- 多个 Atomic 拼起来,不等于业务事务安全。