Java LongAdder在高并发计数场景下的分段锁优化
在并发编程中,统计计数(如点击量、请求数)是极其常见的需求。JDK 8 之前,我们通常使用 AtomicLong 或 synchronized 来实现线程安全计数。然而,在极高的并发场景下,这两种方案都会遇到性能瓶颈。LongAdder 是 JDK 8 引入的高性能计数器,它通过“分段锁”机制解决了这一问题。
核心原理:分段锁与空间换时间
AtomicLong 依赖 CAS(Compare-And-Swap)自旋。当并发量巨大时,大量线程会同时修改同一个变量,导致 CAS 反复失败,CPU 进行大量无效的自旋空转,严重浪费计算资源。
LongAdder 的核心思想是分散热点。它不再让所有线程争抢同一个变量,而是内部维护了一个 Cell 数组(类似于并发分段)。当线程需要计数时,它会通过哈希算法映射到其中一个 Cell 上进行累加。这就像一个银行大厅,原本只有一个柜台(AtomicLong),大家排长队;现在开了多个备用窗口(Cell 数组),客户分流办理,效率自然大幅提升。
最终的结果通过将所有 Cell 的值以及基础值 base 相加得到。
$$ Sum = Base + \sum_{i=0}^{n} Cell_i $$
以下流程展示了 LongAdder 处理并发请求的内部逻辑:
方案对比:选择最适合的工具
在面对不同的并发场景时,我们需要在 AtomicLong、synchronized 和 LongAdder 之间做出选择。下表详细对比了它们的特性:
| 方案 | 实现机制 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| AtomicLong | CAS 自旋(无锁) | 实现简单,非阻塞 | 高并发下 CPU 自旋严重,吞吐量低 | 低并发竞争,或需要全局唯一实时序列号 |
| synchronized | 重量级锁(JDK 1.6 后优化) | 语法简单,JVM 优化后性能尚可 | 线程阻塞涉及上下文切换,有资源开销 | 一般同步控制,非极度高频的计数 |
| LongAdder | 分段累加(Cell 数组) | 极高并发下吞吐量极高 | sum() 并非强一致性(统计期间可能有变动) |
高并发统计、计数、监控上报场景 |
实操指南:如何使用 LongAdder
在单机高并发环境下,优先使用 LongAdder 替代 AtomicLong。以下是具体的使用步骤。
1. 创建计数器实例
引入 java.util.concurrent.atomic.LongAdder 包。实例化 一个 LongAdder 对象用于后续操作。
import java.util.concurrent.atomic.LongAdder;
// 创建一个 LongAdder 计数器,初始值默认为 0
LongAdder counter = new LongAdder();
2. 执行累加或递减操作
调用 increment() 方法将计数加 1,或使用 add(delta) 方法增加指定数值。如果需要减法,调用 decrement() 或传入负数。
// 模拟多线程环境下的计数
Runnable task = () -> {
// 增加计数
counter.increment();
// 等同于 counter.add(1);
// 如果需要减少计数
// counter.decrement();
};
3. 获取当前统计结果
调用 sum() 方法获取当前的累加总值。该方法会遍历所有 Cell 并求和,返回一个 long 类型结果。
// 获取最终的总计数
long totalCount = counter.sum();
System.out.println("Total Count: " + totalCount);
注意:sum() 方法在计算时返回的是一个快照近似值,如果在求和过程中有其他线程正在修改 Cell,可能会导致统计结果不完全精准(例如漏算最后几次修改),但对于统计计量场景(如QPS、TPS),这种微小的误差通常是可以接受的。
代码示例:多线程性能对比
下面的代码模拟了 1000 个线程,每个线程执行 10 万次自增操作,展示了 LongAdder 的基本用法。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
public class LongAdderDemo {
// 定义 LongAdder 计数器
static LongAdder longAdder = new LongAdder();
// 定义 AtomicLong 计数器用于对比
static AtomicLong atomicLong = new AtomicLong(0);
public static void main(String[] args) throws InterruptedException {
int threadCount = 1000;
int timesPerThread = 100000;
// 测试 LongAdder
long start = System.currentTimeMillis();
testLongAdder(threadCount, timesPerThread);
long end = System.currentTimeMillis();
System.out.println("LongAdder Result: " + longAdder.sum() + ", Time: " + (end - start) + "ms");
// 测试 AtomicLong
start = System.currentTimeMillis();
testAtomicLong(threadCount, timesPerThread);
end = System.currentTimeMillis();
System.out.println("AtomicLong Result: " + atomicLong.get() + ", Time: " + (end - start) + "ms");
}
private static void testLongAdder(int threadCount, int times) throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
service.submit(() -> {
for (int j = 0; j < times; j++) {
// 执行递增
longAdder.increment();
}
});
}
service.shutdown();
service.awaitTermination(1, java.util.concurrent.TimeUnit.MINUTES);
}
private static void testAtomicLong(int threadCount, int times) throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
service.submit(() -> {
for (int j = 0; j < times; j++) {
// 执行递增
atomicLong.incrementAndGet();
}
});
}
service.shutdown();
service.awaitTermination(1, java.util.concurrent.TimeUnit.MINUTES);
}
}
补充说明:分布式场景的扩展
上述 LongAdder 仅适用于单机 JVM 内存计数。在分布式微服务架构中,如果需要跨服务统计全局计数,使用 基于 Redis 的 Redisson 框架提供的 RLongAdder 对象。它采用了与 JDK LongAdder 类似的接口和原理,性能远高于分布式锁实现的 AtomicLong。

暂无评论,快来抢沙发吧!