LongAdder 和 AtomicLong 有什么区别?怎么选?

#Java#面试八股

LongAdder 和 AtomicLong 有什么区别?怎么选?

结论先行:AtomicLong 让所有线程 CAS 同一个 value,高并发时单点热点;LongAdder 把计数分散到 Cell 数组,每个线程更新自己的槽,sum() 时再汇总。写多读少选 LongAdder,要精确快照选 AtomicLong。

1. 核心差异

维度AtomicLongLongAdder
数据结构单个 volatile long valuebase + 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。

笔记加载中…