在Java里LongAdder为何适合高并发计数_Java并发性能优化说明

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;
}

}

容易被忽略的两个坑

一个在代码里,一个在 JVM 启动参数上:

  • @Contended 注解在 JDK 8+ 默认无效,必须显式开启 JVM 参数:-XX:+UseContended(JDK 9+ 默认开启,但某些容器镜像或旧版 OpenJDK 可能关闭);否则伪共享防护失效,性能打五折
  • sum() 返回值可能滞后于最新写入——比如线程 A 刚调完 add(1),线程 B 立即调 sum(),未必包含这次更新。这不是 bug,是设计取舍;若需强一致性,应回归 AtomicLong 或加锁