既然已经有了 AtomicInteger,为什么 JDK 还要引入 LongAdder 类?
参考回答
虽然 AtomicInteger
提供了高效的原子操作,但在高并发场景下,LongAdder
进一步优化了性能。LongAdder
是 JDK 8 中新增的类,专为高并发环境下频繁累加的场景设计。
核心区别
AtomicInteger
:基于 CAS(Compare-And-Swap)实现,所有线程竞争同一个变量。LongAdder
:将变量分散到多个槽(Cell)中,不同线程更新不同槽的值,最后通过求和获取结果,从而降低了线程争夺同一变量时的竞争。
详细讲解与拓展
1. AtomicInteger 的性能问题
AtomicInteger
的核心方法(如 incrementAndGet()
)依赖 CAS 实现。
在低并发场景下,CAS 的性能非常高。但在高并发场景下:
- 大量线程竞争更新同一个变量时,失败的线程需要不断重试(自旋),增加 CPU 开销。
- 线程竞争激烈时,缓存一致性协议(Cache Coherence)导致系统性能下降。
2. LongAdder 的设计思想
LongAdder
的核心思想:
“分而治之”,将一个变量分散成多个槽(Cell),不同线程更新不同的槽值,最终将所有槽的值求和得到结果。
- 槽分散:通过内部的数组(
cells[]
),将变量分散到多个槽中。 - 槽独立更新:线程会尽量更新一个独立的槽,减少竞争。
- 最终求和:调用
sum()
方法时,将所有槽的值和基础值合并。
3. LongAdder 的实现原理
核心方法实现:
- 初始化和槽分散:
LongAdder
初始化时,只有一个基础值。高并发时,会动态初始化cells[]
数组,将变量分散。 - 分槽更新: 线程尝试通过 CAS 更新某个槽的值。如果竞争失败,则尝试分配一个新的槽。
- 求和: 调用
sum()
方法时,将基础值与所有槽的值累加。
关键源码片段(JDK 8):
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x))) {
longAccumulate(x, null, uncontended);
}
}
}
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;
}
4. AtomicInteger 与 LongAdder 的性能对比
特性 | AtomicInteger | LongAdder |
---|---|---|
实现方式 | 单变量基于 CAS。 | 多槽(cells[] )分散竞争,最终求和。 |
线程竞争 | 所有线程竞争同一变量。 | 线程竞争不同的槽,降低冲突。 |
适用场景 | 低并发场景,更新次数较少时性能优越。 | 高并发场景下性能更优,特别是频繁累加的操作。 |
内存开销 | 内存占用较少,只有一个变量。 | 内存开销较高,需要分配多个槽(cells[] 数组)。 |
典型应用场景 | 计数器、简单的累加操作。 | 高并发环境下的计数器,例如监控、日志统计等。 |
性能对比示例:
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;
public class PerformanceTest {
public static void main(String[] args) {
AtomicInteger atomicCounter = new AtomicInteger();
LongAdder longAdderCounter = new LongAdder();
int threadCount = 1000;
int iterations = 100000;
// 测试 AtomicInteger
long start = System.nanoTime();
Runnable atomicTask = () -> {
for (int i = 0; i < iterations; i++) {
atomicCounter.incrementAndGet();
}
};
runTest(threadCount, atomicTask);
System.out.println("AtomicInteger time: " + (System.nanoTime() - start));
// 测试 LongAdder
start = System.nanoTime();
Runnable longAdderTask = () -> {
for (int i = 0; i < iterations; i++) {
longAdderCounter.increment();
}
};
runTest(threadCount, longAdderTask);
System.out.println("LongAdder time: " + (System.nanoTime() - start));
}
private static void runTest(int threadCount, Runnable task) {
Thread[] threads = new Thread[threadCount];
for (int i = 0; i < threadCount; i++) {
threads[i] = new Thread(task);
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
预期结果:
- 在低并发环境下,
AtomicInteger
的性能可能更好。 - 在高并发环境下,
LongAdder
的性能显著优于AtomicInteger
。
5. 为什么引入 LongAdder?
- 解决高并发性能瓶颈:
- 在高并发场景下,
AtomicInteger
的性能下降明显,而LongAdder
通过分槽的设计显著降低了线程争夺,提高了性能。
- 在高并发场景下,
- 典型使用场景:
- 高并发场景中的计数器,例如:
- 监控统计(如 QPS 计数器)。
- 日志收集(如记录访问次数)。
- 高并发场景中的计数器,例如:
- 对性能的优化:
- 在线程竞争激烈时,
LongAdder
的吞吐量远高于AtomicInteger
。
- 在线程竞争激烈时,
6. 总结
AtomicInteger
:适合低并发场景,性能更简单,内存占用小。LongAdder
:适合高并发场景,通过分散竞争和分槽设计解决性能瓶颈,但代价是更高的内存开销。