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)可以粗略理解为:

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

java
AtomicInteger retryCount = new AtomicInteger(0);
retryCount.incrementAndGet();

底层思路:

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

优点:

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

代价:

  • 高冲突下会反复自旋重试,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 / 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);
}

为什么适合:

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

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,交易值用锁/数据库
忽略 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

当前仍缺:LongAdderAtomicLong 的热点对比基准、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 拼起来,不等于业务事务安全。