LongAdder在32核以上机器默认“卡住”是因为cells数组初始长度仅2,哈希映射导致大量线程争用少数槽位,且若未启用-XX:+UseContended,伪共享引发缓存同步风暴,使CAS失败率居高不下。

直接换 LongAdder 不等于吞吐翻倍;在 32 核以上机器上,不调优 cells 数组行为,反而可能卡在扩容抖动或伪共享上。
为什么高核 CPU 下默认 LongAdder 会“卡住”
默认初始化的 cells 数组长度是 2,首次竞争触发后才扩容。32 线程并发写入时,大量线程哈希后撞进前几个槽位(getProbe() & (length - 1) 在 length=2 时只有 0/1 两个结果),导致局部 Cell 高频 CAS 失败——本质是“伪分段”,没真正分散到核数级粒度。
更隐蔽的问题是:若 JVM 没开 -XX:+UseContended,@Contended 注解失效,多个 Cell 落在同一缓存行,引发跨核缓存同步风暴(MESI 协议下无效化广播激增)。
- 现象:
Unsafe.compareAndSwapLong失败率未随核数下降,CPU 使用率高但sum()值增长缓慢 - 验证方式:用 JFR 录制,看
java.util.concurrent.atomic.LongAdder::add方法中casBase和casCell的失败比例 - 根本原因:不是线程不够多,而是
cells槽位太少 + 缓存行未隔离 → 竞争从“单点”退化为“多点热点”
手动预热 cells 数组到 2^N ≥ CPU 核数
LongAdder 不提供 public 的初始化数组方法,但可通过反射或“诱导扩容”提前铺开槽位。推荐后者,安全且无侵入:
在应用启动时,用与业务线程同规格的线程池(如 ForkJoinPool.commonPool() 或自定义 ThreadPoolExecutor)并发调用 add(0) 若干次,强制触发扩容至目标大小:
LongAdder counter = new LongAdder(); // 启动时执行一次:让 cells 至少达到 32(适配 32 核) IntStream.range(0, 64).parallel().forEach(i -> counter.add(0)); // 此后 counter.cells.length 通常为 32 或 64
注意:不要用 new Thread(...).start() 方式,避免线程生命周期不可控;也不要用 System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "32"),它只影响并行流,默认仍用 CPU 核数。
- 64 次调用足够触发两次扩容(2→4→8→16→32→64),实际长度取决于竞争节奏,但基本能覆盖 32 核
- 后续所有业务线程的
add()都会映射到更均匀的槽位,哈希冲突概率从 ~50% 降至 ~3%(理论值,实测依负载而定) - 避免在每次请求里做这个操作——只在全局计数器初始化阶段做一次
sum() 调用频率必须低于写入频率一个数量级
在高核环境下,sum() 是 O(n) 操作,n 是 cells.length。若你把 cells 扩到 64,每次 sum() 就要遍历 64 个 Cell + base,再加 volatile 读开销。当写入 QPS 达 10w+ 时,每毫秒都 sum() 会吃掉可观 CPU 时间,甚至拖垮整个采集链路。
- 监控场景:用后台定时任务(如 ScheduledExecutorService)每 5 秒调一次
sumThenReset(),而非实时拉取 - 限流判断:别用
sum() > threshold做开关,改用add(1); if (counter.sum() > threshold) { rollback(); }这类两段式逻辑——但注意这已破坏最终一致性语义 - 压测观察点:关注 GC 日志中是否出现频繁的
longadder.Cell对象晋升(说明cells被反复重建,是初始化或复用不当的信号)
reset() 不能用于高频清零,sumThenReset() 也非原子快照
高频调用 reset() 会逐个设置每个 Cell.value = 0 再设 base = 0,中间窗口期可能被新写入覆盖;而 sumThenReset() 虽“读+清零”看似原子,但它只对当前看到的 cells 快照操作,若此时有线程正往某个刚被清零的 Cell 写入,该增量不会计入本次 sum,也不会丢失——只是延迟到下次 sum() 才体现。
这意味着:在 100ms 窗口内做“采样-清零-上报”,实际统计值可能漏掉最多 1 个写入批次(尤其在突增流量下,cells 尚未扩容完成时)。
- 正确做法:用
sumThenReset()+ 后台线程控制采样周期,接受毫秒级误差;不要试图靠它实现精确滑动窗口 - 绝对禁止:在
reset()后立刻sum()并断言为 0 —— 因为其他线程可能已在清零过程中写入新值 - 最易忽略点:
LongAdder实例本身无状态回收机制,长期运行后cells可能膨胀(如从 64 → 128),但业务流量回落时它不会自动缩容,内存占用只增不减

















