LongAdder 和 AtomicLong 的区别?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
原子类掌握度:面试官不仅仅是想知道这两个类 API 的区别,更是想知道你是否理解
AtomicLong的 CAS 机制,以及它在高并发下的瓶颈在哪。 -
分段锁思想:考察你是否理解
LongAdder用 "空间换时间" + "分散热点" 的思路来解决 CAS 竞争问题,这本质上是 ConcurrentHashMap 分段锁 思想的简化版。 -
选型能力:考察你能否根据实际场景(读多写少 vs 写多读少、是否需要精确的即时值)做出正确的技术选型,而不是无脑选 "性能更好" 的那个。
核心答案
一句话:AtomicLong 用一个 value 做 CAS,所有线程竞争同一个变量;LongAdder 把值分散到多个 Cell 中,每个线程只 CAS 自己的 Cell,最后求和。高并发写场景下 LongAdder 性能远超 AtomicLong。
| 对比维度 | AtomicLong |
LongAdder |
|---|---|---|
| 底层原理 | 单变量 CAS | 分散 Cell + CAS + 求和 |
| 高并发写性能 | 一般(热点竞争严重) | 极好(分散竞争) |
| 读取性能 | O(1),直接返回 value |
需遍历 Cell 求和 |
| 内存占用 | 少(1 个 value) |
多(Cell 数组) |
| 适用场景 | 写入较少、需要精确即时读 | 高频写入、允许最终一致性读取 |
JDK 版本 |
JDK 5+ |
JDK 8+ |
深度解析
一、AtomicLong 的瓶颈在哪?
AtomicLong 的核心就是一个 volatile long value,每次 increment() 都是通过 CAS 更新这个唯一的变量:
// AtomicLong.incrementAndGet() 本质
public final long incrementAndGet() {
// 无限循环 CAS
while (true) {
long current = get();
long next = current + 1;
if (compareAndSet(current, next)) {
return next;
}
// CAS 失败,自旋重试
}
}
这代码看着没毛病。但你想想,100 个线程同时 CAS 同一个变量,同一时刻只有一个能成功,剩下 99 个都得自旋重试。并发越高,CAS 失败率越高,重试越多,CPU 就在这空转。这就是典型的 伪共享(False Sharing) + 热点竞争 问题。
二、LongAdder 怎么解决的?
LongAdder 的思路特别巧妙——既然一个变量抢不过来,那就搞多个,大家各写各的,最后要结果的时候再求和。
上图展示了 LongAdder 的内部结构。核心机制拆解如下:
base:没有竞争时(通常是单线程),直接 CAS 写入base,和AtomicLong一样Cell[]数组:一旦发生 CAS 竞争,就创建 Cell 数组。每个线程通过Thread ID哈希到不同的Cell,各自 CAS 自己的 Cell,彻底消除竞争sum()求和:调用.sum()时遍历所有 Cell 求和:base + cells[0] + cells[1] + ...
这个设计思路和 ConcurrentHashMap 的分段锁异曲同工——把一把锁拆成多把锁,让冲突概率成倍下降。
三、Cell 的伪共享优化
这块是加分项。Cell 数组中每个 Cell 只有几个字节,但它们在内存中是连续排列的。而 CPU 缓存行一般是 64 字节,这就意味着 多个 Cell 可能落在同一个缓存行里。一个 Cell 被修改,整个缓存行失效,其他 Cell 跟着遭殃——这就是伪共享。
LongAdder 的解法很粗暴也很有效:给 Cell 加 @sun.misc.Contended 注解,让 JVM 自动在 Cell 两侧填充空白字节,确保每个 Cell 独占一个缓存行。
// LongAdder 内部 Cell 类(简化)
@sun.misc.Contended
static final class Cell {
volatile long value;
// ...
}
就这么一个注解,高并发下性能提升非常明显。JDK 中不少高性能并发类都用了这个技巧。
四、什么时候用谁?
这个很实际:
// 场景 1:接口 QPS 计数器 → LongAdder(高频写入,偶尔读取展示)
LongAdder requestCounter = new LongAdder();
requestCounter.increment(); // 请求进来 +1
long qps = requestCounter.sum(); // 每秒采样一次,求和
// 场景 2:序列号生成器 → AtomicLong(需要精确的唯一值,每次读取都要绝对准确)
AtomicLong idGenerator = new AtomicLong(0);
long nextId = idGenerator.incrementAndGet(); // 拿到的就是精确的递增值
简单说:高频累加计数用 LongAdder,需要精确的即时值用 AtomicLong。
有个坑要注意:LongAdder 的 .sum() 不是强一致的。你调用 .sum() 的时候,可能有线程正在往 Cell 里写,所以求和的结果和 "那一瞬间的真实值" 可能有微小偏差。对于计数器场景完全够用,但如果你要拿这个值做精确判断(比如限流判断),就得掂量掂量了。
面试高频追问
-
LongAdder的.sum()为什么不是强一致的?因为
.sum()求和的时候没有加锁,遍历 Cell 的过程中可能有其他线程正在更新 Cell。这就导致求和结果可能是 "过去某个时刻的近似值"。这是典型的 最终一致性 思路,用精度换性能。 -
LongAccumulator和LongAdder的关系?LongAdder只支持加法运算(increment、add),而LongAccumulator支持自定义的二元运算(加减乘除、max、min等)。LongAdder本质上是LongAccumulator的一个特化版本。 -
Cell 数组会无限扩容吗?
不会。Cell 数组容量上限是 CPU 核心数(准确说是
2 的幂次方,最大不超过NCPU)。因为最多也就这么多线程能真正并行执行,再多 Cell 也没用。
常见面试变体
- "为什么高并发下
LongAdder比AtomicLong快?" - "
LongAdder的原理是什么?" - "什么是伪共享?
LongAdder是怎么解决的?"
记忆口诀
选型:高频累加用 LongAdder,精确即时用 AtomicLong
原理:一个变量抢不过,拆成多个各自写,最后求和就完事("分而治之,聚而求和")
总结
AtomicLong 和 LongAdder 的本质区别在于:前者是 "单点 CAS",后者是 "分散 Cell + 求和"。LongAdder 用空间换时间,把热点竞争打散,在高并发写场景下性能碾压 AtomicLong。但 LongAdder 的 .sum() 不是强一致的,需要精确值的场景还是得用 AtomicLong。面试时把 "分散热点 + 伪共享优化 + 最终一致性" 这三点讲清楚,这道题就稳了。
