LongAdder的cells数组扩容触发条件是:当前数组长度小于CPU核数且在Cell上CAS失败;此时先CAS设置cellsBusy为1加锁,再将数组扩容为原长2倍,并复制原有数据。

LongAdder 的 cells 数组扩容触发条件是什么
扩容不是按写入次数,而是由线程竞争失败次数驱动的。当某个线程在尝试更新当前 Cell 时 CAS 失败(说明该 Cell 正被其他线程高频修改),且当前 cells 数组未满(长度 NCPU)、数组未被其他线程正在扩容、且 cells 引用未变更,就会触发扩容。
关键点:
-
NCPU是 JVM 启动时读取的Runtime.getRuntime().availableProcessors()值,不可运行时修改 - 扩容是 2 的幂次增长(
newCell[n ),初始为 2,最大到 <code>NCPU - 扩容本身不阻塞写操作:失败线程会 fallback 到
base或重试其他Cell,不会卡死
为什么高核 CPU 下 LongAdder 吞吐量能接近线性提升
核心在于 cells 数组长度上限与 CPU 核心数对齐,让每个物理核心(或超线程)大概率映射到独立 Cell,从而消除缓存行争用。
实操中要注意:
- 如果
NCPU = 64(如 AMD EPYC),cells最多扩容到 64 个,但实际活跃Cell数取决于线程 probe 哈希分布,未必填满 - 线程数远超
NCPU(比如 200 个计数线程)时,probe 哈希冲突概率上升,部分Cell仍会成为热点 - 启用
-XX:+UseContended(JDK 8u60+ 默认开启)才能让@Contended注解生效,避免Cell数组内伪共享
如何确认你的 LongAdder 正在有效利用多 Cell
不能只看 sum() 结果,要观测内部状态。调用 cells.length 和 longValue() 对比可判断是否已走出单 base 阶段:
LongAdder adder = new LongAdder();
// …… 高并发写入后
System.out.println("cells length: " + getCellsLength(adder)); // 需反射或 JOL 工具
System.out.println("sum: " + adder.sum());
更实用的方式是用 JFR(Java Flight Recorder)抓取 jdk.LongAdderUpdate 事件,或通过 Unsafe 反射读取 cells 字段(仅限调试):
-
cells == null:纯base模式,低并发或初始化阶段 cells.length == 2 || 4 || 8 ... :正在逐步扩容,竞争温和-
cells.length == NCPU且sum() >> base:多 Cell 已充分参与,吞吐压榨到位
容易被忽略的三个性能断点
即便开了 64 核,LongAdder 也可能卡在非预期环节:
- 线程创建/销毁开销远大于计数本身 —— 改用固定大小线程池,避免每请求 new Thread
- 频繁调用
sum()(尤其在循环里)会遍历整个cells数组,且每次都要 volatile 读 —— 若只需最终值,攒一批再读;若需监控,改用异步采样 -
base值在扩容期间可能被多个线程同时更新(因为 fallback 路径未加锁),导致少量重复计数 —— 这属于设计允许的误差,但若业务强依赖精确中间态(如限流阈值判断),就不能用LongAdder,得切回AtomicLong或加额外同步

















