LongAdder 和 AtomicLong 有什么区别?怎么选?
结论先行:AtomicLong 让所有线程 CAS 同一个 value,高并发时单点热点;LongAdder 把计数分散到 Cell 数组,每个线程更新自己的槽,sum() 时再汇总。写多读少选 LongAdder,要精确快照选 AtomicLong。
1. 核心差异
| 维度 | AtomicLong | LongAdder |
|---|---|---|
| 数据结构 | 单个 volatile long value | base + Cell[](分段累加) |
| 写路径 | 所有线程 CAS 同一地址,失败自旋 | 优先更新自己的 Cell,冲突才扩容 |
| 高并发写性能 | 差(冲突随线程数急剧上升) | 好(冲突概率被摊薄) |
| 读路径 | O(1),读到精确值 | O(n) 遍历汇总,且弱一致 |
| 内存占用 | 一个对象 | 一个 Cell 数组 + 缓存行填充 |
| 适用 | 序列号、状态标记、低并发计数 | QPS/调用量统计、埋点 |
2. 热点是怎么被打散的
AtomicLong:所有线程抢同一个地址
T1 ─┐
T2 ─┼──▶ [ value = 100 ] ← CAS 只有一个人成功,其余重试
T3 ─┘
LongAdder:线程按 probe 散列到不同 Cell
T1 ──▶ [Cell0 = 5]
T2 ──▶ [Cell1 = 7] sum() = base + Cell0 + Cell1 + Cell2
T3 ──▶ [Cell2 = 3]
LongAdder 在第一次 CAS 失败后,会为当前线程分配(或复用)一个 Cell,此后该线程只更新自己的 Cell;只有在 Cell 数量超过 CPU 核数、或数组需要扩容时,冲突才会重新出现。
3. 用法与一致性差异
// AtomicLong:每次 get 都是精确的当前值
AtomicLong seq = new AtomicLong();
long next = seq.incrementAndGet(); // 严格递增,可当序列号
// LongAdder:写快,但 sum() 是"某一瞬间的汇总",不是原子快照
LongAdder counter = new LongAdder();
counter.increment();
counter.add(10);
long total = counter.sum(); // 汇总时其他线程可能正在改 Cell
long snapshot = counter.sumThenReset();// 汇总并清零,适合周期性上报
关键一点:sum() 不保证并发精确。统计类指标(QPS、成功数)可以接受,账务类数值绝对不能用它。
4. 性能量级(16 线程各 1000 万次自增,示意)
| 实现 | 相对耗时 | 说明 |
|---|---|---|
synchronized | 最慢 | 每次都要抢 monitor |
AtomicLong | 中等偏慢 | 单点 CAS,冲突多时大量自旋 |
LongAdder | 最快 | 分段后几乎无冲突 |
单线程下的 AtomicLong | 比 LongAdder 快 | 没有分段开销,直接 CAS 成功 |
最后一行很关键:低竞争时 LongAdder 反而更慢,因为它多了一次 Cell 探测和汇总开销。
5. 常见追问
追问一:LongAdder 为什么能减少冲突?
把"一个计数器"变成"一组计数器"。线程通过 Thread 上的 threadLocalRandomProbe 散列到不同 Cell,只要两个线程不落在同一个 Cell 上就不冲突;Cell 数组的大小会按需翻倍,直到接近核数。
追问二:sum() 为什么可能不准?
它先读 base,再遍历所有 Cell 累加,这个过程中别的线程还在写。所以它给的是"弱一致的近似值",要精确值就得自己加锁或改用 AtomicLong。
追问三:Cell 为什么要做缓存行填充?
为了防止伪共享:如果两个 Cell 落在同一个 64 字节缓存行,一个线程改 Cell0 会让另一个线程的 Cell1 缓存失效,分段就白做了。JDK 用 @Contended 注解给 Cell 补齐缓存行。
追问四:什么时候必须用 AtomicLong?
需要严格递增序列号、需要 compareAndSet 的条件更新、需要读到精确即时值。LongAdder 没有 compareAndSet,也不保证读到最新总和。
追问五:还有别的选择吗?
LongAccumulator(自定义累加函数)、AtomicIntegerArray(分段数组)、或者直接用数据库/Redis 的计数器(跨进程场景)。
追问六:LongAdder 的 Cell 数组会无限增长吗?
不会。Cell 数组最大长度受"不超过 CPU 核数"的约束(cells.length > NCPU 时不再扩容),冲突时转而依赖 CAS 重试。所以在核数有限的机器上,它的内存占用是有上界的。
小结
AtomicLong 是"一个账本大家排队记",LongAdder 是"每人一个小本子最后汇总"。选择只看两个问题:写并发高不高、读需不需要精确。前者高就 LongAdder,后者要精确就 AtomicLong。