在 Java 并发编程中,线程安全的计数操作是极其常见的需求。JDK 提供了 AtomicLong 和 LongAdder 两种主要工具。虽然它们都能实现原子计数,但在高并发场景下的表现却天差地别。本文将深入探讨两者的底层原理、性能差异及最佳实践。
1. 核心区别概览
简单来说,AtomicLong 是所有线程竞争同一个变量进行 CAS(Compare-And-Swap)更新;而 LongAdder 则将值分散到多个单元格(Cell)中,每个线程只更新自己对应的 Cell,最后通过求和得到总值。
| 特性 | AtomicLong | LongAdder |
|---|---|---|
| 底层机制 | 单变量 CAS 自旋 | 分段计数(Cell 数组)+ 最终求和 |
| 高并发写性能 | 较差(热点竞争严重) | 极佳(分散竞争,吞吐量高) |
| 内存占用 | 低(仅 1 个 volatile long) | 高(维护 Cell 数组,空间换时间) |
| 数据一致性 | 强一致,支持原子读-改-写 | 最终一致性,sum() 非原子快照 |
| 适用场景 | 低并发、需精确即时值、序列号生成 | 高频写入、统计监控、写多读少 |
2. AtomicLong 的性能瓶颈
AtomicLong 的核心是一个 volatile long value。其递增操作如下:
public final long incrementAndGet() {
while (true) {
long current = get();
long next = current + 1;
if (compareAndSet(current, next)) {
return next;
}
// CAS 失败,继续自旋重试
}
}
在低并发下,这种机制非常高效。然而,当大量线程同时尝试修改同一个变量时:
- 热点竞争:同一时刻只有一个线程能 CAS 成功。
- CPU 空转:失败的线程进入
while(true)循环不断重试,消耗大量 CPU 资源却不产生有效工作。 - 性能天花板:随着线程数增加,CAS 失败率急剧上升,吞吐量不再增长甚至下降。
3. LongAdder 的设计思想:分散热点
LongAdder 借鉴了 ConcurrentHashMap 的分段锁思想,采用 “空间换时间” 和 “分散热点” 的策略来解决竞争问题。
3.1 内部结构
LongAdder 内部维护两个部分:
- base:基础值。在无竞争或低并发时,直接通过 CAS 更新 base,行为类似 AtomicLong。
- Cell[] 数组:当检测到竞争时,初始化 Cell 数组。每个 Cell 也是一个简单的计数器。
3.2 工作流程
- 无竞争时:线程直接 CAS 更新
base。 - 有竞争时:
- 系统根据线程的哈希值(通常基于
ThreadLocalRandom.getProbe()),将线程映射到Cell数组中的一个特定槽位。 - 线程只对自己映射到的那个 Cell 进行 CAS 更新。
- 由于不同线程大概率映射到不同的 Cell,竞争被大幅分散,CAS 成功率显著提高。
- 系统根据线程的哈希值(通常基于
- 求和时:调用
sum()方法,遍历所有 Cell 并将它们的值与 base 累加:result = base + cell[0] + cell[1] + ...
3.3 消除伪共享(False Sharing)
这是 LongAdder 高性能的关键细节之一。
- 问题:CPU 缓存以“缓存行”(Cache Line,通常 64 字节)为单位加载。如果多个 Cell 对象位于同一个缓存行中,一个线程修改某个 Cell 会导致整个缓存行失效,其他访问相邻 Cell 的线程必须重新从内存加载数据,导致性能下降。
- 解决:LongAdder 内部的
Cell类使用了@sun.misc.Contended注解。该注解指示 JVM 在 Cell 对象前后填充空白字节,确保每个 Cell 独占一个缓存行,从而彻底消除伪共享带来的性能损耗。
4. 关键注意事项:最终一致性
LongAdder 的 sum() 方法不是原子操作。
- 在遍历 Cell 数组求和的过程中,其他线程可能正在更新某些 Cell 的值。
- 因此,
sum()返回的是当前时刻的近似值,而非严格的瞬时快照。 - 对于监控统计(如 QPS、PV)等场景,这种微小的误差完全可以接受。
- 警告:如果你需要基于当前计数值做出严格的业务决策(如精确限流、金融交易扣减),不能使用 LongAdder,应选用
AtomicLong或加锁机制。
5. 选型指南:什么时候用谁?
✅ 选择 LongAdder 的场景
- 高并发计数统计:如 Web 服务的 QPS/TPS 统计、接口调用次数、错误日志计数。
- 写多读少:更新操作极其频繁,而读取总和的操作相对稀疏(例如每秒上报一次监控数据)。
- 对实时精度要求不高:允许数据存在短暂的不一致,追求极致的写入吞吐量。
// 示例:高并发请求计数
LongAdder counter = new LongAdder();
public void handleRequest() {
counter.increment(); // 高性能累加
// ... 业务逻辑
}
public long getQPS() {
return counter.sum(); // 低频读取,获取近似总和
}
✅ 选择 AtomicLong 的场景
- 需要精确的即时值:如生成全局唯一序列号、订单号。
- 原子性读-改-写:需要基于当前值进行判断并更新,例如
compareAndSet、getAndIncrement(需要返回旧值用于逻辑判断)。 - 低并发或读写均衡:竞争不激烈时,AtomicLong 内存占用更小,代码更简单,且无额外开销。
// 示例:生成唯一 ID
AtomicLong idGenerator = new AtomicLong(0);
public long getNextId() {
// 必须保证返回的值是唯一的、递增的
return idGenerator.incrementAndGet();
}
6. 总结
- AtomicLong 适用于低竞争或需要强一致性、原子性返回值的场景。它是“单点突破”,简单直接。
- LongAdder 适用于高并发、写多读少的统计场景。它通过分段计数和消除伪共享技术,以少量的内存开销换取了巨大的性能提升。
在实际开发中,如果是做监控埋点、流量统计,请优先使用 LongAdder;如果是做业务逻辑控制、ID 生成,请坚持使用 AtomicLong。理解两者的本质区别,有助于我们在高并发系统中做出更合理的性能优化决策。
评论区