longadder不用volatile long累加是因cas争抢同一地址引发总线风暴;它采用分段cell数组,线程分散写入不同cell,仅求和时串行读取,降低竞争;cell通过@contended防伪共享,扩容时用cellsbusy锁并支持协助迁移。

LongAdder 为什么不用 volatile long 做累加?
因为 volatile long 的 getAndAdd 底层依赖 CAS,高并发下大量线程反复争抢同一个内存地址,会触发总线锁或缓存一致性协议(如 MESI)频繁广播,导致“总线风暴”——CPU 花在同步上的开销远超计算本身。
LongAdder 换了思路:不争一个地址,而是分多个 Cell,让线程尽量往不同 Cell 上写。只有最终求和时才串行读一遍所有 Cell,把竞争从“每次累加”降为“偶尔求和”。
-
Cell是个内部静态类,每个实例持有一个volatile long value,但彼此内存地址错开(通过@sun.misc.Contended防伪共享) - 初始无
Cell,第一次add会先尝试 CAS 更新 base 字段;失败才初始化 cells 数组 - cells 数组长度总是 2 的幂,用
hash & (length - 1)定位槽位,避免取模开销
Cell[] 数组扩容时机与风险
扩容不是按需即时进行的,而是在线程发现冲突(CAS 失败)且当前数组非空、未在扩容中时,才尝试 cellsBusy 自旋锁 + CAS 双重检查后扩容。
关键点在于:扩容期间新来的 add 操作可能直接退回到 base 累加,也可能帮着迁移旧 Cell —— 这是 LongAdder “尽力而为”设计的一部分,不保证强一致性,但保障无锁与高吞吐。
- 扩容倍增(
length ),最大到 CPU 核心数上限(<code>NCPU),避免过度分配 - 若
cellsBusy == 1,说明正被其他线程持有扩容锁,当前线程不会阻塞,而是转去更新base - 扩容失败(如 CAS
cellsBusy失败多次)会放弃,下次再试;不会抛异常,也不会死等
get() 和 sum() 的区别在哪?
get() 就是 sum(),两者完全等价。LongAdder 没有单独维护一个“当前值”字段,所有读操作都必须遍历 cells 数组并加上 base。
这意味着:即使没写入,get() 也不是 O(1);它的时间取决于 cells.length,最坏是 O(N),N 是 cells 数组长度(通常 ≤ CPU 核数)。
- 没有缓存值,所以
get()总是反映“尽力聚合后的近似最新”,但不承诺实时性 - 如果业务需要严格单调递增或精确瞬时值(比如限流阈值判断),
LongAdder不适合,得换AtomicLong - 注意:
sum()不加锁,但遍历时若其他线程正在扩容或新建Cell,可能漏掉刚创建还没赋值的槽位(value=0),属于设计允许的误差
什么时候该避开 LongAdder 的 Cell 分段机制?
当你的场景里“读远多于写”,或者“写操作本身极轻量但要求低延迟响应”,LongAdder 反而更重——每次 get() 都要循环读数组,而 AtomicLong.get() 是单次 volatile 读。
另外,如果线程数远少于 CPU 核数(比如只有 2–3 个 worker 线程),分段收益几乎为零,还多了 hash 计算、数组查找、空指针判断等分支逻辑。
- 单元测试中模拟高并发写?别用 LongAdder —— 测试难以断言中间态,
AtomicLong行为确定、易验证 - 日志计数器、监控指标这类“最终一致即可”的场景,才是
Cell分段真正发挥价值的地方 - 注意 JVM 参数:
-XX:-RestrictContended在 JDK 8u20+ 后默认开启,否则@Contended注解无效,Cell仍可能伪共享
实际用的时候,最容易忽略的是:LongAdder 的“快”,只在写竞争激烈 + 读不频繁 + 线程数足够多时成立。拿它当普通计数器用,反而可能更慢,也更难 debug。










