longadder通过线程本地探针哈希定位cells槽位、@contended避免伪共享、懒初始化及2的幂长度实现写竞争分散;sum()需遍历cells故非o(1),且返回近似值,不适用于强一致性场景。

LongAdder 的 cells 数组怎么分散写竞争?
它不靠锁或队列,而是用线程本地探针(ThreadLocalRandom.getProbe())算哈希,再对 cells.length - 1 取模定位槽位。每个线程尽量往自己“专属”的 Cell 上写,避免所有线程挤在同一个 base 地址上自旋。
关键点在于:Cell 类加了 @sun.misc.Contended 注解,强制每个 Cell 占满一个 CPU 缓存行(通常是 64 字节),彻底规避伪共享——否则哪怕映射到不同数组索引,只要落在同一缓存行,更新仍会互相失效、触发 MESI 协议广播。
- 初始无竞争时,所有操作走
base,和AtomicLong行为一致 - 第一次 CAS 失败后才懒初始化
cells,不是一上来就分配数组 -
cells长度始终是 2 的幂,保证取模可用位运算(hash & (n-1)),更快
sum() 为什么不是 O(1),且不能频繁调用?
sum() 必须遍历整个 cells 数组,把每个 Cell.value 加上 base 才得出结果。这个过程不加锁、不阻塞写,所以返回的是某一时刻的近似快照,不是强一致值。
如果每毫秒都调 sum(),比如做实时监控轮询,开销会迅速上升:假设 cells 已扩容到 64 个,每次都要读 65 个 volatile long;而 AtomicLong.get() 是单次内存读,O(1) 且精确。
- 高并发下
cells可能达几十甚至上百项,sum()耗时随数组长度线性增长 - 若在
while (adder.sum() 循环里用它作退出条件,可能永远不退出——因为中间值滞后 - 监控类场景建议每秒/每 5 秒调一次,而非毫秒级
为什么 LongAdder 不提供 compareAndSet()?
因为它压根没维护“当前全局精确值”这个概念。所有写操作分散在多个 Cell 上,没有单一权威 source of truth;sum() 是只读聚合,不可用于条件判断。
如果你需要基于当前计数值做原子决策(比如“剩余库存 > 0 才扣减”),LongAdder 无法满足——它连“当前值”都定义不清。这种场景必须用 AtomicLong 或更高层的同步机制。
-
LongAdder没有compareAndSet()、getAndIncrement()、weakCompareAndSet()等方法 - 试图封装一个带 CAS 的
LongAdder子类会破坏其设计前提,得不偿失 - 真要混合读写逻辑,不如直接用
AtomicLong,或拆成“累加用 LongAdder + 决策用 AtomicLong”两套变量
低并发下 LongAdder 反而更慢?
没错。2~4 个线程持续更新时,LongAdder 的分支判断(先试 base,失败再找 cells)、probe 初始化、数组索引计算,都比 AtomicLong 直接一次 CAS 多几条指令。
它的优势只在“写远多于读 + 并发线程数明显超过 CPU 核心数”时才显现。实测中,8 线程以上持续递增,吞吐量通常拉开 3 倍起;但单线程下 LongAdder.increment() 比 AtomicLong.incrementAndGet() 慢 10%~15%。
- 别为了“听起来高级”盲目替换,尤其在日志计数器、配置加载等低频场景
- JVM 逃逸分析可能让
AtomicLong的 volatile 读被优化,但LongAdder的数组访问几乎无法优化 - 如果应用大部分时间处于低负载,突发高峰才高并发,
LongAdder的自适应扩容机制依然有效,不必担心冷启动问题
真正容易被忽略的是:它解决的是「写热点」,不是「读延迟」。你把 AtomicLong 换成 LongAdder 后,sum() 变慢了、语义变弱了、API 更受限了——这些代价不是 bug,是设计取舍。用之前先问一句:我到底在争什么?是每秒十万次 increment,还是每毫秒一次 get?











