LongAdder通过“base+cells[]”分段结构分散CAS竞争,线程哈希映射到不同Cell更新,并用@Contended避免伪共享,从而降低自旋密度、缓解CPU空转与缓存一致性风暴。

CAS 在高并发下确实容易引发 CPU 空转和伪共享问题,这不是理论风险,而是真实可测的性能瓶颈。关键不在于“用不用 CAS”,而在于“怎么用、用在哪儿、是否配套优化”。
CPU 空转(自旋开销)是怎么发生的?
当多个线程频繁竞争同一个原子变量(比如 AtomicInteger 计数器),CAS 失败后会立即重试——这个“重试”不是挂起等待,而是循环检测、反复比对、不断调用底层 compareAndSwap 指令。线程始终运行在用户态,不释放 CPU,导致:
- 单核或有限核心下,大量线程挤占执行时间,实际吞吐没涨,CPU 使用率飙升
- 失败线程越多,无效自旋越密集,尤其在写热点集中(如秒杀计数、全局请求统计)时尤为明显
- JVM 不会主动干预这种自旋,它只认“线程没阻塞”,所以监控里看不到线程阻塞,却看到 CPU 持续 90%+ 占用
为什么 LongAdder 能缓解空转?
它把“一个值”拆成“一组 Cell”,让不同线程大概率落在不同缓存行上操作:
- 初始只有一个 base 值,类似 AtomicInteger;争抢激烈时自动扩容 Cells 数组
- 线程通过哈希选择某个 Cell 更新,成功概率大幅提升,失败重试次数显著下降
- 最终求和只需累加 base + 所有非空 Cell,读多写少场景下代价极低
本质上是把“单点竞争”变成“分散竞争”,不是消除自旋,而是降低自旋密度。
立即学习“Java免费学习笔记(深入)”;
伪共享(False Sharing)为什么跟着 CAS 出现?
CAS 操作本身不导致伪共享,但 AtomicLong 或 Cell 这类高频更新对象,若成员变量紧挨着布局在内存中,就极易踩坑:
- CPU 缓存以 cache line(通常 64 字节)为单位加载;一个 line 里放多个 volatile 变量,哪怕只改其中一个,也会让整条 line 失效
- 多个线程更新不同 Cell,但如果它们的
value字段落在同一 cache line,就会相互驱逐缓存,引发频繁的总线同步 - JDK 8 的
Cell类用@sun.misc.Contended注解强制填充字段间隔,就是专为隔离 cache line 设计
实战中怎么规避这两类开销?
不能只靠换工具,得结合场景选策略:
- 写热点极高(如每秒百万级计数)→ 优先用
LongAdder或Striped64衍生类,避免死守AtomicLong - 需要强一致性读(如实时余额)→ 仍可用
AtomicLong,但需评估自旋是否已影响响应延迟,必要时引入退避策略(如失败后Thread.yield()或短暂停顿) - 自定义无锁结构时 → 主动对齐字段,用
@Contended(需 JVM 启动参数-XX:-RestrictContended)、或手动 padding 填充 56 字节保证单字段独占 cache line - 监控要落地 → 不只看 QPS 和错误率,加采样线程栈,抓到
Unsafe.compareAndSwapLong高频出现在 top 耗时栈里,就是空转信号
这些开销不是 CAS 的缺陷,而是它轻量、无锁特性的伴生代价。理解它,才能用好它。


















