AtomicLong在16线程以上性能陡降是因CAS争抢同一地址导致高自旋失败;LongAdder通过Cell[]分段+线程哈希映射降低冲突,以sum()近似和换取高吞吐,适用于监控等弱一致性场景。

为什么 AtomicLong 在 16 线程以上就明显卡住
不是它写得不好,而是设计上就扛不住多线程争抢同一个 CAS 地址。Unsafe.compareAndSwapLong 失败后立刻自旋重试,16 个线程同时调用 incrementAndGet(),大概率只有 1 个成功,其余 15 个白占 CPU 做无用比较。压测时线程堆栈里反复出现 getAndAddLong → compareAndSwapLong 循环,就是典型信号。
现象是吞吐不随线程数线性增长,反而在 8–16 线程区间陡降;实测在 32 核机器上,16 线程并发 100 万次 increment,AtomicLong 耗时可能是 LongAdder 的 5 倍以上。
LongAdder 是怎么靠 Cell[] 把冲突打散的
它不让你都挤在 base 上,而是用线程自己的 threadLocalProbe 哈希值,映射到 Cell[] 某个槽位(类似 probe & (cells.length - 1))。首次冲突触发数组初始化(默认长度 2),后续同一线程大概率复用同一槽位,不同线程则大概率落在不同槽位。
- 没竞争时:直接走
casBase,和AtomicLong行为一致,零额外开销 - 有竞争时:各写各的
Cell.value,CAS 失败率大幅下降 - 扩容极谨慎:仅当多个线程反复冲突且当前数组未满时才扩容,避免小流量下浪费内存
sum() 返回不准?这不是 bug,是明确取舍
sum() 遍历当前所有活跃 Cell 并累加 base,但遍历过程中其他线程仍在并发更新——结果是“近似和”,不是原子快照。这不叫缺陷,是换高吞吐付出的合理代价。
适用场景非常具体:
- 监控指标(QPS、错误计数、PV/UV)——毫秒级误差完全可接受
- 限流窗口计数、日志打点行数统计——写多读少,读频次远低于写频次
- 绝对不能用于库存扣减、金融对账等需要强一致读写的场景
如果边写边频繁调用 sum()(比如每 10ms 采样一次),结果可能漏掉部分增量,建议改为异步聚合或降低读频次。
四个最容易踩的实际坑
别以为把 AtomicLong 换成 LongAdder 就万事大吉:
-
reset()不是原子清零:先逐个设Cell.value = 0,再设base = 0,中间可能被其他线程add()新值 - 不支持
decrementAndGet()或compareAndSet():只能add(-1),无法做“小于 100 才减”这类条件操作 - 低流量后突增并发:
cells可能仍为null或长度不足,需靠真实流量触发扩容,冷启动阶段性能不如预期 - 别在 request scope 或
ThreadLocal里 new ——退化成单线程计数,彻底浪费分段能力
长期运行后 Cell[] 不会自动收缩,若维度多(如按 API 路径缓存),要用 ConcurrentHashMap + 合理过期策略,否则内存持续增长。










