1. 引言
在 Java 应用中,常要处理高并发下的计数操作,比如统计请求量、日志统计、监控指标等。传统的 AtomicLong 通过 CAS 方式确保并发安全,但在高并发场景下性能急剧下降。
Java 8 引入了 LongAdder,通过热点分散和分段结构维持性能稳定,是实现高性能计数类的重要创新。本文将深入解析该类的设计和实现,并提供实际应用指引。
2. LongAdder 的设计初衷与核心概念
2.1 设计初衷
AtomicLong 采用了 CAS (Compare-And-Swap) 操作来确保并发安全,但所有线程都对同一个 value 进行操作,导致性能瓶颈。
LongAdder 的设计初衷是“换空间换时间”:
将一个值分散到多个 cell 中存储,避免多个线程争夺同一个内存地址
操作时随机选择 cell 进行 CAS 操作
统计时将全部值相加
2.2 核心概念
Base: 初始值的基础系数
Cells: 分散数组,作为热点分散的存储单元
Cell: 内部静态类,包装 long 值,并通过 Unsafe 提供 CAS 功能
Hashing: 通过 ThreadLocalRandom 维护分散性
3. 基本使用示例
import java.util.concurrent.atomic.LongAdder;
public class CounterDemo {
private static final LongAdder counter = new LongAdder();
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 100_000; i++) {
counter.increment();
}
};
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(task);
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("Final count: " + counter.sum());
}
}
运行结果将显示正确的计数结果:Final count: 1000000
4. 源码深度解析
为了深入理解 LongAdder
如何在高并发场景下实现优越性能,本章将从类结构、核心字段、关键方法入手,逐步剖析其源码实现。我们不仅展示源码片段,还对每一段逻辑进行详细解释,力求做到真正的源码级掌握。
4.1 类结构概览
LongAdder
继承自抽象类 Striped64
,其本质是通过分段(striped)的方式,减轻多个线程对同一个变量的竞争压力。
public class LongAdder extends Striped64 {
public void add(long x) { ... }
public void increment() { add(1L); }
public void decrement() { add(-1L); }
public long sum() { ... }
public void reset() { ... }
public long sumThenReset() { ... }
}
我们可以看到 LongAdder
提供了多个方法,便于执行递增、递减、求和和重置操作。
其父类 Striped64
提供了两个关键字段:
volatile long base
: 基础计数值。volatile Cell[] cells
: 分段数组,数组中的每一个Cell
用来分担高并发下的更新压力。
4.2 分段机制与核心思想
核心思想是将原本集中在一个变量(如 AtomicLong.value
)上的竞争,分散到多个 Cell
上。
初始情况下,线程尝试更新
base
字段。如果更新
base
失败(意味着竞争严重),则初始化cells
数组。每个线程根据其唯一的哈希值(probe)选择一个
Cell
更新。多线程更新分布在不同的
Cell
上,显著降低了 CAS 冲突率。
4.3 构造器行为
public LongAdder() {}
构造方法非常简洁,不初始化任何内部结构。原因在于:
base
默认就是 0,无需初始化。cells
数组采用懒加载策略,仅在并发冲突时才创建。
这意味着 LongAdder 的资源开销是动态触发的,对单线程使用非常友好。
4.4 add() 方法详解
核心的 add(long x)
方法是实现计数操作的关键,它的源码如下:
public void add(long x) {
Cell[] cs; long b, v; int m; Cell c;
if ((cs = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
if (cs == null || (m = cs.length - 1) < 0 || (c = cs[getProbe() & m]) == null ||
!(uncontended = c.cas(v = c.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
我们逐行解析:
Cell[] cs; long b, v; int m; Cell c;
定义变量,用于存储当前
cells
数组、base 值、Cell 等中间结果。
if ((cs = cells) != null || !casBase(b = base, b + x))
如果
cells
数组已经存在(说明并发量大),或者base
CAS 更新失败,则进入分段处理逻辑。
boolean uncontended = true;
标记当前线程是否成功地独占了对应的
Cell
。
if (cs == null || ... || !(uncontended = c.cas(...)))
检查
cells
是否为空,数组大小是否有效,对应的Cell
是否存在,CAS 是否成功。若任一条件失败,调用
longAccumulate()
方法处理并发冲突和扩容。
4.5 CAS 与 Unsafe 实现
LongAdder
的原子操作并未使用 java.util.concurrent.atomic
提供的封装类,而是直接调用底层的 sun.misc.Unsafe
实现 CAS。
casBase 方法
protected final boolean casBase(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}
比较当前对象的
base
字段是否等于cmp
,如果是则设为val
。BASE
是base
字段在内存中的偏移量,通过静态代码块初始化。
Cell.cas 方法
static final class Cell {
volatile long value;
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, VALUE, cmp, val);
}
}
每个
Cell
实际上是一个封装了long
值的原子变量。也使用
Unsafe
进行 CAS 更新。
4.6 哈希分散策略(getProbe 和 advanceProbe)
getProbe()
获取当前线程的“探针值”,用于定位到 cells
中的槽位:
static final int getProbe() {
return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
PROBE
是线程本地哈希值的偏移量。如果发生哈希冲突,会调用
advanceProbe()
生成新的哈希值。
该策略确保不同线程尽可能命中不同的 Cell
,减少热点重叠。
4.7 longAccumulate() 的核心职责
当 add()
中的尝试全部失败,进入 longAccumulate()
方法:
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
// 实现极为复杂,包含:
// 1. 初始化 cells 数组
// 2. 尝试占用 cell
// 3. CAS 失败时判断是否需要扩容
// 4. 如果扩容失败,则重试或反复调整 probe
// 最终确保某个 cell 或 base 成功被更新
}
这是整个 LongAdder
并发控制的核心实现,具备如下能力:
分段数组的懒初始化
冲突检测与槽位迁移
分段数组的动态扩容(每次翻倍)
不断重试直到成功更新某个槽位
5. 与 AtomicLong / LongAccumulator 的对比分析
在本章中,我们将深入比较 LongAdder
、AtomicLong
和 LongAccumulator
三个类,从它们的设计理念、实现结构、典型用法和性能表现等多个维度进行全面剖析,帮助开发者选择最适合自己业务场景的并发计数工具。
5.1 设计目的与适用场景
类名 | 设计初衷 | 适用场景 |
---|---|---|
AtomicLong | 通用型原子计数器,线程安全但容易产生争抢 | 并发量较低或写操作不频繁 |
LongAdder | 针对高并发优化,弱一致性计数器 | 高并发下计数统计,指标收集等 |
LongAccumulator | 可指定累加规则的扩展版本,功能更灵活 | 复杂累加逻辑(如求最值) |
5.2 类结构与接口对比
方法名 | AtomicLong | LongAdder | LongAccumulator |
get() / sum() | ✔ | ✔ | ✔ |
increment() | ✔ | ✔ | ✖ |
add(x) | ✔ | ✔ | ✔ |
reset() | ✖ | ✔ | ✔ |
sumThenReset() | ✖ | ✔ | ✔ |
自定义运算逻辑 | ✖ | ✖ | ✔ |
可见,AtomicLong
接口最简单,而 LongAdder
更适合计数类需求,LongAccumulator
则支持通用型聚合操作。
5.3 使用示例对比
AtomicLong 示例
AtomicLong atomicLong = new AtomicLong();
atomicLong.incrementAndGet();
System.out.println(atomicLong.get());
LongAdder 示例
LongAdder adder = new LongAdder();
adder.increment();
System.out.println(adder.sum());
LongAccumulator 示例
LongAccumulator max = new LongAccumulator(Long::max, Long.MIN_VALUE);
max.accumulate(10);
max.accumulate(20);
System.out.println(max.get()); // 输出 20
5.4 性能实测对比
我们设计如下性能对比测试,统计多线程下 1 亿次递增操作的耗时:
public class CounterBenchmark {
static final int THREADS = 20;
static final int TASKS_PER_THREAD = 5_000_000;
public static void main(String[] args) throws InterruptedException {
benchmark("AtomicLong", () -> {
AtomicLong counter = new AtomicLong();
runThreads(() -> {
for (int i = 0; i < TASKS_PER_THREAD; i++) {
counter.incrementAndGet();
}
});
});
benchmark("LongAdder", () -> {
LongAdder counter = new LongAdder();
runThreads(() -> {
for (int i = 0; i < TASKS_PER_THREAD; i++) {
counter.increment();
}
});
});
}
static void runThreads(Runnable task) throws InterruptedException {
Thread[] threads = new Thread[THREADS];
for (int i = 0; i < THREADS; i++) {
threads[i] = new Thread(task);
threads[i].start();
}
for (Thread thread : threads) thread.join();
}
static void benchmark(String label, Runnable task) throws InterruptedException {
long start = System.currentTimeMillis();
task.run();
long duration = System.currentTimeMillis() - start;
System.out.println(label + " time: " + duration + " ms");
}
}
运行结果(大致):
AtomicLong time: 2200 ms
LongAdder time: 480 ms
说明 LongAdder
在高并发写操作下的性能显著优于 AtomicLong
。
5.5 一致性语义差异
AtomicLong.get()
始终返回最新的值,具备强一致性。LongAdder.sum()
可能存在轻微误差(最终一致性),因为它需要将所有Cell
的值汇总。LongAccumulator.get()
同样基于分段聚合,非瞬时一致。
5.6 内存占用差异
类名 | 是否分段数组 | 内存占用趋势 |
AtomicLong | ✖ | 常量级 |
LongAdder | ✔ | 线程数越多,内存越大 |
LongAccumulator | ✔ | 同上 |
这意味着 LongAdder
和 LongAccumulator
是以“换空间为时间”的策略。
5.7 线程安全与并发吞吐对比
指标 | AtomicLong | LongAdder | LongAccumulator |
并发写性能 | ❌ | ✅ | ✅ |
一致性强度 | ✅ | ❌ | ❌ |
可扩展性 | ❌ | ✅ | ✅ |
内存可控性 | ✅ | ❌ | ❌ |
小结
使用
AtomicLong
的情况:更新频率低、对一致性要求极高。使用
LongAdder
的情况:统计类操作、高并发、对实时性要求不高。使用
LongAccumulator
的情况:聚合非加法逻辑(如最大值、乘积等)。
6. 常见问题与解决方案
虽然 LongAdder
在高并发场景下表现出色,但在实际开发中仍然可能遇到一些令人困惑的问题或陷阱。本章将列举和解析若干典型问题,并提供解决建议,帮助开发者更稳健地使用该类。
6.1 sum() 返回不准确?
问题描述
不少开发者在线上使用 LongAdder
后发现,sum()
返回的结果和实际期望值有出入,尤其是在频繁并发更新时。
原因分析
这是由于 LongAdder.sum()
方法会将 base
和 cells[]
中所有槽位的值进行累加,但由于没有加锁,可能在合并过程中有线程正在更新某个 Cell
,导致结果略有误差。
示例重现
LongAdder adder = new LongAdder();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
adder.increment();
}
}).start();
}
Thread.sleep(1000);
System.out.println("Sum: " + adder.sum()); // 不一定是 1000000
解决建议
如果对准确性要求极高,应在统计窗口内使用同步方法控制更新节奏。
或使用
sumThenReset()
并搭配锁机制使用,避免读写冲突。
6.2 重置失败或出现负数?
问题描述
在调用 reset()
后再次使用 LongAdder
,发现计数值出现负数或重置失败。
原因分析
reset()
和并发写操作之间没有强同步,可能在 reset 过程中某些线程仍在写入 base
或 Cell
,导致重置无效。
示例
LongAdder adder = new LongAdder();
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
pool.execute(() -> {
adder.increment();
adder.reset();
});
}
pool.shutdown();
pool.awaitTermination(1, TimeUnit.SECONDS);
System.out.println(adder.sum()); // 有可能 < 0
解决建议
reset()
仅适用于无并发写入时。多线程场景推荐使用
sumThenReset()
并在外部加锁以保证一致性。
6.3 内存占用增长?
问题描述
在某些长期运行的应用中发现 LongAdder
占用内存不断增长,甚至触发 GC 问题。
原因分析
cells[]
容器是懒初始化且动态扩容的,每当线程竞争激烈且 CAS 失败频繁,就可能触发扩容,最终形成大量 Cell
槽位。
扩容后不会自动收缩,导致空间一直占用。
解决建议
若并发度固定,建议限制线程数量。
不复用的
LongAdder
可通过sumThenReset()
定期清理状态。定期重建对象释放内存也是一种折中方案。
6.4 不适合强一致性场景
问题描述
某些系统中使用 LongAdder
统计计数值后直接用于业务判断,如用户限流、访问控制等,结果出现不一致或误判。
原因分析
LongAdder.sum()
结果存在弱一致性风险,不适合作为高安全等级的判断依据。
解决建议
对强一致性场景应改用
AtomicLong
或基于锁的计数器。LongAdder
更适合作为指标采样或趋势分析的工具。
6.5 无法序列化?
问题描述
尝试将 LongAdder
序列化用于持久化或远程传输时报错:NotSerializableException
。
原因分析
LongAdder
和其父类 Striped64
并未实现 Serializable
接口,内部的 Cell
也不可序列化。
解决建议
若需持久化,仅序列化其
sum()
值。或将统计逻辑与存储逻辑解耦,只记录结果。
小结
问题类别 | 根因 | 建议做法 |
---|---|---|
sum() 不准确 | 弱一致性合并 | 结合锁或 sumThenReset 控制周期 |
reset 异常 | 缺乏并发同步 | 尽量只在无写入时调用 reset |
内存增长 | cells 懒加载 + 扩容无回收 | 控制并发、定期清空或重建实例 |
强一致性误用 | sum 非强一致 | 改用 AtomicLong 或同步方案 |
无法序列化 | 未实现 Serializable 接口 | 仅存 sum 结果或自定义序列化逻辑 |
7. 性能调优建议
尽管 LongAdder
在默认配置下已具备极强的并发性能,但在真实项目中,合理的参数配置和场景适配仍能进一步提升其表现,尤其是在高吞吐、低延迟和资源敏感型系统中。本章将围绕以下几个方面展开探讨:
线程数量与
cells[]
容量关系JVM 参数配置影响
热点线程绑定与 NUMA 架构优化
与线程池搭配的优化技巧
对比实测及调优示例
7.1 cells[] 容量与并发度匹配
LongAdder
的核心优化机制之一在于通过 cells[]
数组分散写热点,每个线程在写入时尽量避免与其他线程竞争同一个槽位。但这个数组容量是懒初始化且扩容触发的,因此并发线程数高于 cells.length
时才会触发新的扩容。
建议策略
若并发线程数已知或固定(如线程池),建议提前并发“预热”,让
cells[]
尽早扩容至合理容量,减少运行时竞争。
for (int i = 0; i < poolSize; i++) {
new Thread(() -> adder.increment()).start();
}
在服务初始化阶段执行一次并发写操作预热。
7.2 JVM 参数调优建议
某些 JVM 参数会对并发类库中的锁竞争、内存布局、线程调度产生间接影响,以下是部分建议配置:
参数 | 含义 | 推荐配置(仅供参考) |
---|---|---|
-XX:+UseNUMA |
启用 NUMA 感知 | 开启(对多 CPU 核心有益) |
-XX:+UseBiasedLocking |
使用偏向锁,减少无竞争场景的开销 | Java 8 默认开启 |
-XX:+AlwaysPreTouch |
启动时预触所有页,优化内存访问 | 对低延迟系统建议开启 |
-XX:ParallelGCThreads |
GC 并行线程数 | 设置为 CPU 核心数 |
7.3 与线程池搭配的性能建议
使用 LongAdder
时,如果结合线程池执行统计类任务,应注意以下几点:
避免线程池大小远大于 CPU 核数,否则线程频繁切换反而导致竞争加剧。
若任务短小频繁,可考虑
ForkJoinPool
的commonPool
,其内部也使用ThreadLocal
进行线程槽绑定优化。
示例
ExecutorService pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
LongAdder adder = new LongAdder();
for (int i = 0; i < 1000; i++) {
pool.submit(() -> adder.increment());
}
7.4 避免不必要的 sum 操作
虽然 sum()
是无锁操作,但它遍历所有 Cell
进行汇总,如果频繁调用,会导致缓存失效和 CPU cache miss。
优化建议
设置统计周期(如 1 秒)进行采样式汇总。
配合定时任务批量读取。
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
System.out.println("count: " + adder.sumThenReset());
}, 0, 1, TimeUnit.SECONDS);
7.5 多核绑定与 NUMA 结构优化
在多 CPU NUMA 系统中,不合理的线程调度可能导致远程内存访问,性能降低。
优化措施
使用操作系统层面的 CPU 亲和性绑定(taskset / numactl)将热点线程固定至同一 NUMA 节点。
使用线程池绑定核心的方式进行调度(如自定义
ThreadFactory
)。
7.6 性能实测与对比
我们做一个带与不带优化的对比,场景为:20 个线程递增 5000 万次。
未优化
LongAdder time: 680ms
预热 + 限定线程池 + sum 周期采样
LongAdder time: 460ms
说明合理的参数设置和线程管理策略可以进一步提升 LongAdder 在高并发下的表现。
小结
优化点 | 建议手段 |
初始化扩容 | 并发预热 |
JVM 参数调整 | 开启 NUMA、合理线程绑定 |
线程池设置 | 限制线程数与核心数相当,避免上下文切换过多 |
汇总频率控制 | 使用定时采样方式避免频繁 sum 操作 |
NUMA 架构优化 | 固定线程亲和性、避免远程访问 |