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)可以粗略理解为:
“只有当当前值仍然是我刚才看到的旧值时,我才把它改成新值;否则说明有人抢先修改了,我就重试。”
java
AtomicInteger retryCount = new AtomicInteger(0);
retryCount.incrementAndGet();底层思路:
- 读取当前值
- 在本地算出新值
- 通过 CAS 尝试写回
- 如果期间别人改过了,就重试
优点:
- 无锁,低到中等冲突时吞吐很好
- 不需要线程挂起 / 唤醒
代价:
- 高冲突下会反复自旋重试,CPU 可能被烧掉
- 天然更适合单变量,不适合长临界区、多字段事务
2. Atomic* 家族:按数据形态选
AtomicInteger / AtomicLong
适合:
- 点赞数、任务完成数、重试次数、下载进度累计
- 精确递增序列(更常见是
AtomicLong)
java
AtomicLong requestId = new AtomicLong(1000);
long id = requestId.incrementAndGet();AtomicBoolean
适合:
- 一次性启动门闩
- “只允许一个线程成功开启”
java
AtomicBoolean started = new AtomicBoolean(false);
if (started.compareAndSet(false, true)) {
startWorker();
}AtomicReference<T>
适合:
- 不可变配置快照整体替换
- 小型状态机的原子切换
java
AtomicReference<State> state = new AtomicReference<>(State.IDLE);
boolean ok = state.compareAndSet(State.IDLE, State.RUNNING);3. LongAdder:为高冲突计数分桶减压
LongAdder 的核心思路不是“一个格子大家抢”,而是:
- 低冲突时先更新基值
- 冲突高时把热点拆散到多个 cell
- 读取时再把多个 cell 汇总起来
java
LongAdder qps = new LongAdder();
qps.increment();
long current = qps.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);
}为什么适合:
- 单变量累加
- 需要精确结果
- 读写频率中等
2. 只允许启动一次的后台任务
java
AtomicBoolean started = new AtomicBoolean(false);
void startIfNeeded() {
if (started.compareAndSet(false, true)) {
doStart();
}
}适合:
- SDK 初始化
- 后台 worker 启动门闩
- 避免重复订阅 / 重复上报
3. 后端热点指标统计
java
LongAdder successCounter = new LongAdder();
void onSuccess() {
successCounter.increment();
}适合:
- QPS、成功数、失败数、命中数
- 写多读少
- 指标用途重于事务用途
4. 不可变配置快照切换
java
AtomicReference<AppConfig> configRef = new AtomicReference<>(AppConfig.defaultConfig());
void reload(AppConfig fresh) {
configRef.set(fresh);
}前提:
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 拼起来,不等于业务事务安全。