LongAdder在高并发下比AtomicLong快的根本原因是将竞争从单点base分散到多个独立缓存行的Cell,避免MESI协议下的缓存行失效与自旋失败;它适合最终一致场景,不适用于依赖中间值的原子操作或强一致性需求。

为什么LongAdder在高并发下比AtomicLong快得多
根本原因不是“它用了更高级的算法”,而是它把原本所有线程挤在同一个内存地址上抢 base 的局面,拆成了多个独立缓存行上的“小战场”。当每秒几万线程同时调用 increment() 时,AtomicLong 的 CAS 会因缓存行失效(MESI协议下的总线风暴)反复失败、自旋空转;而 LongAdder 让大部分线程各自更新自己映射到的 Cell,冲突概率骤降。
- CPU 缓存行通常是 64 字节,
@Contended注解确保每个Cell独占一行,彻底规避伪共享 - 线程通过
ThreadLocalRandom.getProbe()获取哈希值,再与cells.length - 1取模定位槽位——天然具备隔离性,无需额外同步 - 只有初始化
cells数组或扩容失败时,才退化为更新base,属于“尽量分散、必要时兜底”
什么时候该用LongAdder,什么时候不该用
它不是 AtomicLong 的无脑替代品,语义和适用边界很明确:
- 适合:监控指标(QPS、错误数)、批量任务完成计数、限流器中的请求累加——只要求“最终一致”,不依赖中间值做条件判断
-
不适合:
- 需要
compareAndSet()或getAndIncrement()这类原子读-改-写操作 - 用
sum()结果作为 while 循环退出条件(如while (counter.sum() ),可能永远不退出 - 单线程或极低并发(2~3 线程),此时
AtomicLong更轻量,LongAdder反而多一层分支判断和数组寻址开销
- 需要
正确使用LongAdder的三个关键动作
改起来就三处,但漏掉任一细节都可能引入隐蔽问题:
-
初始化:直接
new LongAdder()即可,无需参数;不要手动 new 多次来“重置”,应调用reset() -
累加:用
increment()或add(long x),别误用set(long)(该方法不存在) -
读取:必须用
sum(),不是get()(LongAdder没有get()方法);注意sum()是非原子快照,返回的是遍历当前cells+base的瞬时和
public class ApiCounter {
private final LongAdder totalRequests = new LongAdder();
private final LongAdder failedRequests = new LongAdder();
public void onRequest(boolean success) {
totalRequests.increment();
if (!success) failedRequests.increment();
}
public long getFailureRate() {
long total = totalRequests.sum(); // ✅ 正确
long failed = failedRequests.sum();
return total == 0 ? 0 : (failed * 100) / total;
}}
立即学习“Java免费学习笔记(深入)”;
容易被忽略的两个坑
一个在代码里,一个在 JVM 启动参数上:
-
@Contended注解在 JDK 8+ 默认无效,必须显式开启 JVM 参数:-XX:+UseContended(JDK 9+ 默认开启,但某些容器镜像或旧版 OpenJDK 可能关闭);否则伪共享防护失效,性能打五折 -
sum()返回值可能滞后于最新写入——比如线程 A 刚调完add(1),线程 B 立即调sum(),未必包含这次更新。这不是 bug,是设计取舍;若需强一致性,应回归AtomicLong或加锁











