在 Java 并发编程中,我们都知道 AtomicLong 依靠 CAS 乐观锁实现无锁原子计数,规避了 synchronized 重量级锁的内核态切换、上下文切换、缓存失效等性能损耗。
但很多人只知其优、不知其弊:超高并发下,AtomicLong 会出现严重的 CPU 空转问题,大量线程白白消耗 CPU 时间片,吞吐量断崖式下跌。
而 JDK8 推出的 LongAdder,正是为解决这个痛点而生。很多同学疑惑:计数器只是修改同一个逻辑数值,不像 ConcurrentHashMap 可以拆分不同 key 分片,LongAdder 凭什么优化高并发竞争?
本文结合Debug 实操案例+底层原理图解+优缺点对比+场景选型,一次性讲透两者核心区别,彻底终结面试和实战困惑。
一、先复盘:AtomicLong 的核心痛点(高并发 CPU 空转)
1. AtomicLong 底层机制
AtomicLong 内部仅有一个核心变量:volatile long value,所有线程的累加、更新操作,全部争抢同一个内存地址、同一个 CPU 缓存行。
执行逻辑非常简单:
-
读取当前 value 值;
-
通过 CAS 比较并交换,尝试更新数值;
-
CAS 成功直接返回,失败就无限自旋重试。
低并发场景下,CAS 成功率极高,无锁设计性能碾压重量级锁,优势极其明显。
2. 高并发致命问题:单点 CAS 风暴 + CPU 空转
当大量线程(几十、上百个)同时执行累加操作时,灾难就会出现:
-
所有线程竞争同一个 value 地址,同一时刻仅有 1 个线程能 CAS 成功;
-
其余 90% 以上的线程全部 CAS 失败,进入无限自旋循环;
-
这些失败线程没有阻塞、没有休眠,持续占用 CPU 时间片反复重试,却无法推进业务逻辑;
-
同时频繁触发 CPU 缓存行失效、总线缓存同步,造成严重的总线风暴,进一步拖垮整机性能。
核心总结:AtomicLong 只是把「线程阻塞的开销」换成了「CPU 空转的开销」,低并发赚性能,高并发亏 CPU。
3. 通俗场景类比
一个柜台(唯一 value 变量),上百人排队办理业务:同一时间只能办 1 个人,剩下所有人不排队、不休息,一直在柜台门口反复尝试挤进去,白白消耗体力(CPU 资源),完全是无效消耗。
二、LongAdder 核心解决方案:空间换时间,热点分流
很多人的误区:计数器是同一个数值,没法像 ConcurrentHashMap 一样拆分。
LongAdder 的天才设计:逻辑上是一个计数器,物理上拆成多份独立变量。
它不解决 CAS 失败问题,而是大幅降低 CAS 冲突概率,从根源减少线程空转。
1. LongAdder 底层核心结构(继承 Striped64)
LongAdder 摒弃了单一 value 变量,核心由三部分组成:
-
base:基础累加值,低并发时直接 CAS 更新,等价于 AtomicLong,走快速路径;
-
Cell\[\] cells:分段计数数组,高并发竞争激烈时启用,存储分散的计数值;
-
cellsBusy:自旋标记位,仅用于保护 cells 数组的初始化、扩容,不参与计数更新。
同时 Cell 内部通过 @sun.misc.Contended 缓存行填充,彻底解决伪共享问题,每个 Cell 独占一条 CPU 缓存行,互不干扰。
2. 核心工作原理
-
低并发:直接 CAS 修改 base 值,和 AtomicLong 完全一致,零额外开销;
-
高并发 CAS 失败:不再自旋重试 base,而是初始化/扩容 cells 数组;
-
线程分流:每个线程通过本地伪随机 probe 哈希,分配到不同的 Cell 位置,只 CAS 修改自己对应的 Cell 值;
-
冲突兜底:单个 Cell 竞争失败时,线程会重新哈希换其他 Cell,不死磕冲突位置,彻底避免无效空转;
-
最终汇总 :获取总值时,通过
sum() = base + 所有 Cell.value 累加,还原逻辑唯一计数值。
3. 和 ConcurrentHashMap 分段锁的本质区别
这是大家最容易混淆的点,直接精准区分:
-
ConcurrentHashMap :分段是为了区分不同数据,不同槽位存储不同 key,是「数据分片+加锁」;
-
LongAdder :所有 Cell 共同累加同一个逻辑数据,无锁、纯 CAS 分流,是「热点拆分+空间换时间」。
三、Debug 实操演示:3 线程并发累加全过程
我们用极简案例,一步步看内存变化,直观感受两者差异,代码可直接复制调试:
java
import java.util.concurrent.atomic.LongAdder;
public class LongAdderDebugDemo {
public static void main(String[] args) throws InterruptedException {
LongAdder adder = new LongAdder();
// 3个线程并发累加1
Thread t1 = new Thread(() -> adder.add(1), "Thread-1");
Thread t2 = new Thread(() -> adder.add(1), "Thread-2");
Thread t3 = new Thread(() -> adder.add(1), "Thread-3");
t1.start();t2.start();t3.start();
t1.join();t2.join();t3.join();
// 汇总最终结果
System.out.println("最终计数总和:" + adder.sum());
}
}
1. 初始状态
base = 0,cells = null,无任何分段数据。
2. 线程1执行(低竞争)
直接 CAS 修改 base 成功,base = 1,cells 仍为 null,快速结束。
3. 线程2、3同时执行(出现竞争)
尝试 CAS 更新 base(1→2)失败,触发 cells 数组初始化:
-
创建长度为 2 的 Cell 数组,初始值均为 0;
-
线程2 哈希命中 cells0,CAS 成功 → cells0 = 1;
-
线程3 哈希命中 cells1,CAS 成功 → cells1 = 1。
4. 最终内存快照
base = 1,cells0 = 1,cells1 = 1
5. sum() 汇总逻辑
sum = base + cells0 + cells1 = 1 + 1 + 1 = 3,结果精准正确。
6. 高冲突兜底场景
若多个线程命中同一个 Cell、CAS 冲突失败:线程不会自旋空转,会重新哈希切换其他 Cell;若整体竞争激烈且数组长度小于 CPU 核心数,自动二倍扩容,持续分流减压。
四、LongAdder 致命缺陷(选型核心关键)
天下没有免费的午餐,LongAdder 用空间换高并发性能,代价非常明显,也是生产中不能无脑替代 AtomicLong 的核心原因:
1. 弱一致性,sum() 非原子快照
sum() 是遍历 base 和所有 Cell 累加得出的结果,遍历过程中,其他线程可以持续修改 Cell 数值。
这意味着 sum() 获取的是「近似值」,不是某一时刻的全局原子快照。
而 AtomicLong.get() 是单次 volatile 读取,是绝对精准的原子一致性快照。
2. 不支持 CAS 自定义修改逻辑
LongAdder 仅支持 add()、increment()、decrement() 简单累加操作,没有 compareAndSet 方法。
无法实现「满足条件才更新、乐观锁校验」的业务逻辑。
3. 占用更多内存空间
需要初始化 Cell 数组,且数组最大扩容至 CPU 核心数,同时缓存行填充会额外占用内存,相比 AtomicLong 单一变量,内存开销大幅增加。
五、AtomicLong vs LongAdder 最终选型场景
看完原理和缺陷,直接记住这套生产、面试通用选型规则即可:
1. 优先使用 AtomicLong 的场景
-
并发量低、竞争不激烈的计数场景;
-
需要精准原子快照,必须获取某一时刻准确数值;
-
需要 CAS 自定义更新逻辑(如余额校验更新、状态条件更新);
-
内存资源敏感,追求极小开销的简单计数。
2. 优先使用 LongAdder 的场景
-
超高并发统计计数(接口 QPS、请求总量、PV/UV、任务统计等);
-
只需要最终统计结果,不要求实时强一致性;
-
线程竞争激烈,AtomicLong 出现 CPU 占用过高、空转严重的场景。
六、全文核心总结(面试满分答案)
-
AtomicLong 问题:单一变量单点 CAS,高并发下大量线程 CAS 失败、无限自旋,造成 CPU 空转、时间片浪费,缓存频繁失效,性能暴跌。
-
LongAdder 解决方案:采用 base + Cell\[\] 分段计数,空间换时间,将单点热点竞争分散到多 Cell 分流 CAS,大幅降低冲突概率,从根源减少 CPU 空转。
-
核心优势:高并发性能远超 AtomicLong,无无效自旋,CPU 利用率极高。
-
核心缺陷:sum() 弱一致性、无 CAS 自定义更新、内存开销大。
-
选型口诀:低并发、要精准、要 CAS 用 AtomicLong;高并发、做统计、不苛求瞬时一致用 LongAdder。