在开发多线程应用时,你是否曾遇到这样的困扰:随着并发量增加,系统性能不升反降?特别是在计数器场景下,本应简单的自增操作却成了性能瓶颈。这正是许多 Java 开发者共同面临的痛点。当线程数超过 CPU 核心数或竞争激烈时,AtomicLong 的 CAS 操作不断失败重试,CPU 使用率飙升,而业务处理效率却直线下降。这也是为什么阿里巴巴在其开发规范中明确推荐使用 LongAdder 来替代传统方案。
LongAdder 是什么
LongAdder 是 Java 8 在java.util.concurrent.atomic
包中引入的一个新的原子性操作类,专为高并发环境下的计数场景设计。与传统的 AtomicLong 相比,它采用了更加优化的内部实现,能够有效减少线程间的竞争,提高并发性能。
为什么需要 LongAdder
在分析 LongAdder 的优势前,我们先了解传统方案 AtomicLong 的问题:
AtomicLong 的性能瓶颈
AtomicLong 主要依赖 CAS(Compare-And-Swap)操作保证原子性。当多线程同时更新同一个计数器时,会出现以下问题:
- 激烈的竞争:所有线程竞争同一个值的更新权
- 频繁的失败和重试:CAS 操作在高并发下失败率高
- CPU 空转:频繁 CAS 重试导致 CPU 资源浪费
- CAS 竞争热点:多线程争抢同一变量更新权,形成系统瓶颈
下面是 AtomicLong 的主要更新方法实现:
public final long incrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}
这种实现在高并发下会导致大量线程自旋等待,形成"CAS 竞争热点"问题。
LongAdder 的实现原理
LongAdder 采用了"分段累加"的设计思想,巧妙地避开了 AtomicLong 面临的竞争问题。
核心设计:分段计数
LongAdder 内部维护了一个基础值 base 和一个 Cell 数组。这里可以把它想象成一个"分布式计数器":
线程哈希计算伪代码:
线程哈希 = ThreadLocalRandom.getProbe();
cellIndex = 线程哈希 & (cells.length - 1); // 位运算提高效率
工作流程如下:
- 无竞争时很简单:所有线程都更新 base 值,性能接近 AtomicLong
- 出现竞争后动态分流:不同线程被分配到不同的 Cell 格子去更新,互不干扰
- 结果计算设计简洁:base 值加上所有 Cell 值的总和
这种设计就像银行柜台:人少时一个窗口就够了(base);人多时开放多个窗口(Cells),每个客户去不同窗口办理,互不影响,效率大大提高。
内存占用对比
LongAdder 采用了空间换时间的设计思路,下面是 AtomicLong 与 LongAdder 的内存占用对比:
Cell 类与伪共享问题
Cell 类是 LongAdder 内部的核心组件,它使用了@Contended
注解避免伪共享问题:
@sun.misc.Contended
static final class Cell {
volatile long value;
Cell(long x) { value = x; }
// CAS更新方法
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
}
伪共享(False Sharing):当多个线程频繁修改位于同一 CPU 缓存行的不同变量时,即使变量无关,也会因缓存一致性协议导致缓存行频繁失效,降低性能。@Contended
通过填充字节使Cell
实例独占缓存行,避免此问题。值得注意的是,在 OpenJDK 中,@Contended
注解默认仅对 JDK 内部类生效,外部应用需通过-XX:-RestrictContended
参数启用。
这就像在超市购物:如果相邻的收银台共用一个出口通道,一个收银台的顾客在结账会影响另一个收银台顾客的通行。@Contended
注解相当于给每个收银台都建立了独立的出口通道,避免了这种无谓的等待。
动态扩容机制
LongAdder 不会一开始就创建很多 Cell,而是按需增长:
- 初始状态节省内存:只有一个 base 值,所有线程都往这里加数
- 竞争时才扩容:当发现 base 更新冲突,才初始化 Cell 数组(初始大小为 2)
- 持续优化分配:线程通过
ThreadLocalRandom.getProbe()
生成哈希值映射到对应 Cell
当 Cell 更新失败时,longAccumulate
方法会:
- 重试更新:通过
ThreadLocalRandom.advanceProbe()
生成新的哈希值,避持续冲突 - 扩容 cells 数组:若重试多次失败,且 cells 长度小于
2^24
,则将数组长度翻倍(2→4→8),扩容上限为2^24
,以避免无限制增长 - 处理极端情况:当扩容后仍冲突,或系统资源紧张时,会通过自旋(
Thread.yield()
)或短暂休眠减少 CPU 占用,避免活锁
需要注意的是,即使在最理想的情况下,当线程数极多时(如接近2^24
),或者哈希冲突严重时,LongAdder 也可能退化为类似 AtomicLong 的竞争模式,只是概率大大降低。
关键代码解析
LongAdder 的核心方法 add()实现(简化版):
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
// 如果Cells数组已初始化 或者 更新base值失败(说明有竞争)
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
// 如果Cells数组未初始化 或 当前线程的Cell未初始化 或 更新Cell失败
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
sum()方法获取最终值:
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
sum()
方法本身是线程安全的(不需要额外同步),但呈现的是弱一致性结果,它在读取时不会阻塞写操作,可能读到部分更新的中间状态(如某Cell
正在被更新时读取其旧值)。这与 AtomicLong 的get()
方法提供的强一致性形成对比。但当所有写操作完成后,多次调用sum()
结果会一致(最终一致性),这对统计类场景(如 QPS、总量计数)已经足够。
工作原理图解
性能测试与对比
下面通过一个简单的性能测试,对比 LongAdder 和 AtomicLong:
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
public class CounterPerformanceTest {
private static final int ITERATIONS = 100000;
public static void main(String[] args) throws Exception {
// 测试不同线程数下的性能
for (int threadCount : new int[]{10, 100, 500, 1000, 2000}) {
System.out.println("测试线程数: " + threadCount);
testAtomicLong(threadCount);
testLongAdder(threadCount);
System.out.println();
}
}
private static void testAtomicLong(int threadCount) throws Exception {
final AtomicLong counter = new AtomicLong(0);
long start = System.currentTimeMillis();
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
for (int j = 0; j < ITERATIONS; j++) {
counter.incrementAndGet();
}
});
}
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.HOURS);
long end = System.currentTimeMillis();
System.out.println("AtomicLong: " + (end - start) + "ms, Result: " + counter.get());
}
private static void testLongAdder(int threadCount) throws Exception {
final LongAdder counter = new LongAdder();
long start = System.currentTimeMillis();
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
for (int j = 0; j < ITERATIONS; j++) {
counter.increment();
}
});
}
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.HOURS);
long end = System.currentTimeMillis();
System.out.println("LongAdder: " + (end - start) + "ms, Result: " + counter.sum());
}
}
测试结果
测试环境:Intel i7-10700K(8 核 16 线程),JDK 11.0.12,OpenJDK 64 位,内存 16GB
可以看到几个有趣的现象:
- 在低并发(10 线程)时,AtomicLong 甚至略快于 LongAdder,这是因为 LongAdder 有额外的判断逻辑
- 随着线程数的增加,AtomicLong 性能直线下降,而 LongAdder 的性能下降相对平缓
- 在 2000 线程的场景下,在测试环境中 LongAdder 的性能约为 AtomicLong 的 7 倍
这就像高峰时段的马路:单车道会越来越堵,而多车道则能保持较高的通行效率。但在车辆稀少时,单车道反而更简单高效。
性能差异原因分析
- 减少争用:LongAdder 通过分散更新不同的 Cell,大大减少了线程间的竞争
- 降低失败率:每个线程更可能操作不同的 Cell,CAS 操作成功率更高
- 提高并行度:多个线程可以并行更新不同的计数单元,而不是串行等待
- 避免伪共享:Cell 类使用了@Contended 注解,避免了缓存行伪共享问题
实际应用场景
LongAdder 特别适合以下场景:
- 高并发计数:如系统运行状态监控、QPS 统计
- 性能指标收集:统计接口调用次数、成功率等
- 限流计数器:短时间内的请求量统计
- 缓存命中率统计:记录缓存命中和未命中次数
选择正确的计数器
根据不同场景选择合适的计数器工具:
// 场景1: 需要原子条件更新的场景 - 使用AtomicLong
AtomicLong sequencer = new AtomicLong(0);
// 生成下一个序列号,同时确保不超过最大值
long nextId = sequencer.updateAndGet(current ->
current < MAX_SEQUENCE ? current + 1 : current);
// 场景2: 高并发计数统计场景 - 使用LongAdder
LongAdder totalRequests = new LongAdder();
// 多线程并发调用
totalRequests.increment();
// 定时任务汇总打印
scheduler.scheduleAtFixedRate(() -> {
System.out.println("当前总请求数: " + totalRequests.sum());
}, 0, 5, TimeUnit.SECONDS);
应用示例:接口监控计数器
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LongAdder;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
public class ApiMetricsCounter {
private final ConcurrentHashMap<String, LongAdder> apiCounters = new ConcurrentHashMap<>();
public void recordApiCall(String apiName) {
// 获取或创建对应API的计数器
apiCounters.computeIfAbsent(apiName, k -> new LongAdder()).increment();
}
public long getApiCount(String apiName) {
LongAdder counter = apiCounters.get(apiName);
return counter == null ? 0 : counter.sum();
}
public void printAllMetrics() {
apiCounters.forEach((api, counter) ->
System.out.println(api + ": " + counter.sum() + " calls"));
}
// 使用示例
public static void main(String[] args) throws Exception {
ApiMetricsCounter metrics = new ApiMetricsCounter();
// 模拟多线程调用
ExecutorService executor = Executors.newFixedThreadPool(100);
for (int i = 0; i < 1000; i++) {
final int index = i % 3;
executor.submit(() -> {
String api = "api" + index;
metrics.recordApiCall(api);
});
}
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
// 打印结果
metrics.printAllMetrics();
}
}
生产环境监控集成
在实际的生产环境中,可以使用 Micrometer 等监控框架来采集 LongAdder 的数据,避免频繁调用sum()
:
// 使用Micrometer监控LongAdder
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.FunctionCounter;
public class MetricsService {
private final LongAdder requestCounter = new LongAdder();
public MetricsService(MeterRegistry registry) {
// 注册LongAdder到Micrometer,自动定期采集数据
FunctionCounter.builder("api.requests", requestCounter, LongAdder::sum)
.description("API请求总数")
.register(registry);
}
public void recordRequest() {
requestCounter.increment();
}
}
LongAdder 的注意事项
虽然 LongAdder 性能优越,但也有一些使用注意事项:
- 内存占用:LongAdder 内部的 Cell 数组会占用更多内存。AtomicLong 只有一个 value 变量(约 24 字节),而 LongAdder 的每个 Cell 因为@Contended 注解占用约 128 字节(一个缓存行大小)。就像高速公路:单车道省地但容易堵,多车道通行效率高但占地多。
- 非精确读取:在高并发更新时调用
sum()
,结果可能不是实时准确的,因为求和过程中可能有新的更新发生。这就像统计进出商场的人数,你在数的过程中可能有人进出,导致结果有轻微偏差。 - 哈希冲突:当线程数远超 Cell 数组大小时,可能多个线程映射到同一个 Cell,造成局部热点。以下是一个简单的工具,帮助你检查 Cell 数组状态:
// 调试用:查看LongAdder的Cell分布情况
// 注意:反射调用非公开API,可能导致兼容性问题,仅用于调试分析
private static void checkCellDistribution(LongAdder adder) {
try {
// 反射获取cells字段
Field cellsField = LongAdder.class.getDeclaredField("cells");
cellsField.setAccessible(true);
Object[] cells = (Object[]) cellsField.get(adder);
if (cells == null) {
System.out.println("cells数组未初始化,所有线程更新base值");
return;
}
System.out.println("Cell数组大小: " + cells.length);
for (int i = 0; i < cells.length; i++) {
if (cells[i] != null) {
Field valueField = cells[i].getClass().getDeclaredField("value");
valueField.setAccessible(true);
long value = (long) valueField.get(cells[i]);
System.out.println("Cell[" + i + "] 值: " + value);
} else {
System.out.println("Cell[" + i + "] 未创建");
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
性能优化建议
若通过调试工具发现某Cell
竞争激烈(如Cell[0]
值远高于其他Cell
),可通过 JVM 参数预设初始cells
大小:
-Djava.util.concurrent.atomic.LongAdder.cellSize=32
此参数需谨慎使用,默认初始cellSize
为 2,扩容策略与线程竞争程度相关。通常不建议设置超过 CPU 核心数的 2-4 倍,盲目增大可能导致内存浪费,建议仅在确认存在严重哈希冲突时调整。
当sum()
调用非常频繁时(如高频监控),可考虑使用sumThenReset()
方法获取并重置计数器,减少多次累加的开销:
long total = counter.sumThenReset(); // 获取当前总数并清零
实际应用建议
基于以上分析,在实际开发中可以遵循以下建议:
- 在高并发统计场景(写多读少)下,优先使用 LongAdder 而非 AtomicLong
- 对于计数器类场景(如统计、度量),使用 LongAdder 能带来显著性能提升
- 需要精确原子操作(如序列号生成)的场景,仍然使用 AtomicLong
- 低并发场景下,两者性能差异不大,可以根据实际需求选择
总结
特性 | AtomicLong | LongAdder |
---|---|---|
并发性能 | 较低,高并发下性能下降明显 | 优秀,线程数越多优势越明显 |
内存占用 | 低(约 24 字节) | 较高(base + N 个 Cell,每个 Cell 约 128 字节) |
精确性 | 实时精确(get() 强一致) | 最终一致(sum() 允许短暂不一致) |
适用场景 | 需要原子条件更新 | 统计类场景(高并发计数) |
实现复杂度 | 简单,基于单个变量的 CAS 操作 | 复杂,涉及 base、Cell 数组、动态扩容、哈希映射等 |
Java 版本 | Java 5+ | Java 8+ |
低并发表现 | 性能略好(实现简单) | 略有额外开销(判断逻辑) |
LongAdder 通过巧妙的分段设计,有效解决了高并发下 AtomicLong 的性能瓶颈。这也是阿里巴巴在 Java 开发手册中推荐使用 LongAdder 的主要原因。
在实际开发中,我们应根据应用场景选择合适的工具。就像选择交通工具:短途可以骑自行车(AtomicLong 简单够用),长途拥堵路段就需要高铁(LongAdder 突破瓶颈)。对于监控、统计类高并发场景,LongAdder 通常是更优选择;而对于需要精确原子操作的场景,AtomicLong 仍然是必要的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。