LongAdder的Cell扩容是竞争触发的懒加载,由cellsBusy自旋锁控制,初始长度为2、倍增扩容,@Contended注解通过JVM填充规避伪共享。

LongAdder 的 Cell 扩容不是“自动”的,而是竞争触发的懒加载+自旋控制
很多人误以为 LongAdder 会像 ConcurrentHashMap 那样定期或定时扩容。实际上,cells 数组从 null 开始,只有在首次 CAS 更新 base 失败(即检测到竞争)时,才尝试初始化;后续扩容也只在 longAccumulate 内部、且当前线程哈希槽位冲突严重时发生。
关键控制变量是 cellsBusy:它用 CAS 实现轻量级自旋锁,值为 0 表示可安全初始化或扩容,非 0 则说明有其他线程正在操作数组结构。这避免了重量级锁,但也意味着扩容时机完全由竞争节奏决定,而非 CPU 核数预设。
- 初始
cells长度为 2(不是 1),之后每次扩容都是 2 倍(如 2 → 4 → 8 → 16…) - 扩容前会检查当前长度是否已 ≥ CPU 核数(通过
Runtime.getRuntime().availableProcessors()),但不强制对齐——这是常见误解 - 扩容失败(如
cellsBusy竞争激烈)时,线程可能退回到更新base,造成短暂热点回退
@Contended 是规避伪共享的硬性保障,不是可选优化
@sun.misc.Contended 注解是 Cell 类生效的关键。它让 JVM 在 Cell 对象前后各填充 128 字节(JVM 8u60+ 默认启用),确保每个 Cell 实例独占至少一个缓存行(64 字节),彻底切断相邻 Cell 之间的缓存行干扰。
但这个机制依赖 JVM 启动参数:-XX:+UseContended(默认开启),否则注解被忽略,Cell 仍按紧凑布局,伪共享风险回归。
- 不要手动实现类似 padding:JVM 对
@Contended的填充策略更稳定,且与 GC 兼容 - 自定义类加
@Contended需指定组名(如@Contended("cell")),但LongAdder内部Cell是无组名的默认组,已足够隔离 - 在容器化环境(如 Docker)中,若 JVM 未正确识别宿主机 CPU 数,
@Contended仍有效——它作用于单个对象内存布局,与核数无关
高核服务器上真正影响伪共享规避效果的是线程哈希分布,不是 Cell 数量
LongAdder 使用 Thread.currentThread().getThreadLocalRandom().nextSecondarySeed()(或 UNSAFE.getInt(Thread, PROBE))生成线程探针值,再通过 (h ^ (h 计算索引。这个哈希逻辑本身不保证均匀,尤其当 cells.length 较小时,多个线程容易哈希到同一 Cell。
因此,在 64 核以上服务器上,如果并发写线程远少于核数(比如仅 8 个线程持续累加),cells 可能长期停留在长度 2 或 4,导致少数几个 Cell 承担全部压力,伪共享虽被单个 Cell 规避,但 CAS 冲突并未减少。
- 观察
cells实际长度:可通过反射读取LongAdder.cells.length,确认是否随竞争上升 - 避免“低线程高核”场景下盲目信任:此时
AtomicLong可能反而更稳——因为无数组维护开销,且伪共享影响已被现代 JVM 缓存优化部分缓解 - 若业务可预估峰值线程数,可在初始化后主动触发一次
add(0)多次,诱导扩容至接近该数量的 2 的幂次
sum() 返回值不是强一致快照,高并发读写混合时需接受误差
sum() 方法遍历 cells 数组并累加所有非空 Cell.value,再加上 base。整个过程无锁,也不冻结数组状态。这意味着:
- 遍历时某个
Cell正被其他线程 CAS 更新,sum()可能读到旧值或新值,属于正常行为 - 若某线程刚完成扩容但尚未写入新
Cell,sum()不会包含该次增量 - 误差范围理论上 ≤ 当前活跃写线程数 × 单次增量(通常为 1),实践中在吞吐优先场景下可接受
这不是 bug,而是设计取舍:用最终一致性换掉读路径的锁开销和缓存行同步成本。如果你需要精确瞬时值,LongAdder 不适用——得切回 ReentrantLock 包裹的普通 long,或接受性能惩罚。










