LongAdder 和 AtomicLong 的区别?
2026年01月14日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
面试官抛出这个问题,通常意在考察以下几个层面:
- 对高并发计数器/累加器实现方案的了解: 面试官想知道你是否了解在 Java 中,除了经典的
AtomicLong,还有性能更优的替代方案LongAdder。 - 对性能瓶颈和优化思路的理解: 这不仅仅是记忆区别,更是考察你是否能理解
AtomicLong在高并发写入场景下的性能瓶颈(即大量线程对同一热点变量的 CAS 竞争),以及LongAdder是如何通过 “空间换时间” 和 “分散竞争” 的思路来解决这个问题的。 - 对底层并发机制的掌握: 深入考察你是否理解
AtomicLong基于Unsafe的 CAS(Compare-And-Swap) 操作,以及LongAdder所借鉴的 分段思想(Striping) 和其对 伪共享(False Sharing) 的优化处理。 - 场景化选型能力: 面试官希望你能够根据具体的业务场景(是频繁写入还是频繁读取,是否需要强一致的精确值)来做出合理的技术选型。
核心答案
AtomicLong 和 LongAdder 都是 Java 并发包(java.util.concurrent.atomic)下用于高并发环境下进行原子性操作的类。它们的核心区别在于 高并发写入场景下的性能 和 读取结果的特性。
简单来说:
AtomicLong内部使用一个volatile long变量,通过 CAS(自旋) 保证原子更新。所有线程竞争同一个变量,在竞争激烈时,大量线程会自旋重试,导致 CPU 开销激增,性能下降。LongAdder则采用了 “分段累加” 的思想。它内部维护了一个Cell[]数组(每个Cell是一个独立的原子计数器)和一个base变量。写操作时,通过哈希算法将线程映射到不同的Cell上进行累加,从而将竞争分散,大大减少了 CAS 冲突。在需要获取最终结果时,再将所有Cell的值和base累加。
因此,在并发写入远多于读取的场景下(例如统计点击数、接口调用次数),LongAdder 的性能显著高于 AtomicLong。但代价是,LongAdder 在读取值时(调用 sum())可能需要合并数据,开销稍大,且它提供的是 最终一致性 的估值,在并发累加过程中读取的值可能不精确。而 AtomicLong 的每次读取都是 强一致性 的精确值。
深度解析
原理/机制
- AtomicLong: 其核心是
private volatile long value;。进行incrementAndGet()等操作时,在一个死循环中不断尝试用Unsafe.compareAndSwapLong方法将value从旧值更新为新值,直到成功。这就是 CAS 自旋。所有线程都盯着这一个value变量,竞争是 集中式 的。 - LongAdder:
- 分散竞争: 它继承自
Striped64。内部有transient volatile Cell[] cells和transient volatile long base。当没有竞争时,操作直接作用在base上。一旦发生竞争(某个线程 CAS 更新base失败),它会初始化或扩展cells数组,并尝试将当前线程通过哈希值映射到数组中的一个Cell槽位上,后续的累加操作主要在自己的Cell上进行。这样,写热点就被分散到了多个Cell中。 - 避免伪共享:
Cell类使用@sun.misc.Contended注解进行缓存行填充。这确保了每个Cell对象会独占一个 CPU 缓存行,防止多个Cell变量被加载到同一缓存行,从而避免因一个Cell更新导致其他Cell缓存失效的 “伪共享” 问题,这是实现高性能的关键细节之一。 - 最终一致性:
sum()方法只是遍历cells数组,将各个Cell的值与base相加。这个求和过程没有加锁,也没有阻止并发的写操作,所以在并发累加过程中调用sum()得到的是一个某一时刻的近似快照,并非精确值。如果需要精确值,可以结合业务考虑在低竞争时段读取,或使用AtomicLong。
- 分散竞争: 它继承自
代码示例
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
public class CounterComparison {
public static void main(String[] args) throws InterruptedException {
// 场景:模拟100个线程,每个线程累加10000次
final AtomicLong atomicCounter = new AtomicLong(0);
final LongAdder adderCounter = new LongAdder();
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
atomicCounter.incrementAndGet(); // AtomicLong 累加
adderCounter.increment(); // LongAdder 累加
}
};
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(task);
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
System.out.println("AtomicLong 最终结果: " + atomicCounter.get()); // 强一致,精确
System.out.println("LongAdder 最终结果: " + adderCounter.sum()); // 最终一致,此时线程已结束,所以精确
}
}
对比分析与最佳实践
| 特性 | AtomicLong | LongAdder |
|---|---|---|
| 核心原理 | 基于 CAS 自旋,单一变量 | 基于 分段累加 (Cell[]),分散竞争 |
| 写入性能 | 高竞争下差(大量自旋) | 高竞争下极优(竞争被分散) |
| 读取性能 | 高(直接读 volatile 变量) | 较低(需要遍历 Cell 数组求和) |
| 数据一致性 | 强一致性,每次读取都是最新精确值 | 最终一致性,sum() 是近似快照,非精确值 |
| 内存占用 | 低 | 较高(维护 Cell 数组) |
最佳实践与选型指南:
- 写多读少,且对读取实时性要求不高: 例如收集统计指标(QPS、错误次数)、计数器等,首选
LongAdder。它的高性能优势在此类场景下发挥得淋漓尽致。 - 读多写少,或需要频繁依赖当前值做决策: 例如生成序列号、需要基于当前值进行条件判断的场合,应使用
AtomicLong。因为它能提供精确、实时的最新值。 - 精确计数的场景: 如库存扣减(需要精确知道还剩多少),虽然
LongAdder最终结果正确,但过程中读取的值不精确,可能影响业务判断,此时通常也更适合AtomicLong或使用锁。
常见误区
- 认为
LongAdder在所有场景下都比AtomicLong好: 错。LongAdder的优势仅在高并发写入场景。在低并发或读多写少的场景,AtomicLong简单可靠,性能可能更好。 - 忽略
LongAdder.sum()的非原子性: 在并发环境下,sum()返回的值可能已经“过时”,不能用于需要强一致性的场景(如作为检查条件)。 - 低估
LongAdder的内存开销:Cell数组和缓存行填充会带来额外的内存消耗,在内存敏感的环境中需纳入考量。
总结
LongAdder 是 JDK 8 引入的、专门针对高并发写入场景优化的计数器,它通过分段累加和避免伪共享的巧妙设计,以牺牲读取性能和强一致性为代价,换取了卓越的写入吞吐量,是 AtomicLong 在高并发写竞争下的优秀替代方案。选型时,务必根据 “读写比例” 和 “数据一致性要求” 来决策。