atomiclong高并发卡在cas自旋,因所有线程争同一value字段;longadder通过base+cells[]分段结构分散热点:线程按哈希映射到不同cell槽位更新,并用@contended避免伪共享。

为什么 AtomicLong 在高并发下会卡在 CAS 自旋上
因为所有线程都在争同一个 value 字段:unsafe.getAndAddLong(this, valueOffset, 1L) 每次都对同一内存地址做 CAS,失败后立即重试。线程数一多(比如 > CPU 核心数),自旋成功率断崖式下跌,CPU 占用飙升但吞吐不涨。
LongAdder 是怎么分散热点的
它不靠一个变量扛压,而是用「base + cells[]」双层结构:
-
base是基础值,低并发时所有操作直接走它,开销≈AtomicLong -
cells是延迟初始化的Cell[]数组,默认长度为 2,扩容按 2 的幂次增长(如 4、8、16) - 每个线程通过
ThreadLocalRandom.getProbe()计算哈希,再用位运算hash & (cells.length - 1)映射到某个Cell槽位,只更新自己的槽 -
Cell内部用@sun.misc.Contended做缓存行填充,避免伪共享——这点常被忽略,但直接影响多核性能
什么时候该切到 LongAdder,而不是硬扛 AtomicLong
不是“并发高就无脑换”,关键看写读比和精度要求:
- 写远多于读(如 QPS 统计、限流计数器)→
LongAdder更合适;读频繁且要精确瞬时值(如库存扣减判断)→ 还得用AtomicLong -
LongAdder.sum()返回的是遍历所有cells和base的累加值,**不是原子快照**:调用过程中其他线程可能正在修改某个Cell,所以它本质是最终一致性的 - 内存占用更高:一个
Cell至少占 128 字节(含填充),16 个槽就是 ~2KB,而AtomicLong就一个 long - 初始化成本低,但首次竞争触发
cells创建时有少量同步开销(casBase失败后才建数组)
实际用法与容易踩的坑
API 看似和 AtomicLong 一样,但行为差异很关键:
- 没有
incrementAndGet()或compareAndSet()—— 它只提供increment()、add(long)、sum()、longValue()(等价于sum()) -
sum()不加锁遍历,但若需强一致性结果(比如做 if 判断),必须自己加同步或改用AtomicLong - 不要在循环里反复调用
sum()做条件判断,这会放大不一致性;更适合“采样统计”“日志打点”类场景 - 如果业务中已有
AtomicLong,替换时注意检查是否依赖了getAndIncrement()的返回值逻辑——LongAdder不支持这种“原子读-改-返”语义
最常被低估的一点:伪共享不是理论问题。在 32 核以上机器上,没 @Contended 的 Cell[] 数组,多个线程写相邻 Cell 可能导致 L1 缓存行反复失效,性能反而不如单个 AtomicLong。JDK 8+ 默认开启 -XX:-RestrictContended 才真正生效,上线前务必确认 JVM 参数。











