既然已经有了 AtomicInteger,为什么 JDK 还要引入 LongAdder 类?

参考回答

虽然 AtomicInteger 提供了高效的原子操作,但在高并发场景下,LongAdder 进一步优化了性能。LongAdder 是 JDK 8 中新增的类,专为高并发环境下频繁累加的场景设计。

核心区别

  • AtomicInteger:基于 CAS(Compare-And-Swap)实现,所有线程竞争同一个变量。
  • LongAdder:将变量分散到多个槽(Cell)中,不同线程更新不同槽的值,最后通过求和获取结果,从而降低了线程争夺同一变量时的竞争。

详细讲解与拓展

1. AtomicInteger 的性能问题

AtomicInteger 的核心方法(如 incrementAndGet())依赖 CAS 实现。
在低并发场景下,CAS 的性能非常高。但在高并发场景下:

  1. 大量线程竞争更新同一个变量时,失败的线程需要不断重试(自旋),增加 CPU 开销。
  2. 线程竞争激烈时,缓存一致性协议(Cache Coherence)导致系统性能下降。

2. LongAdder 的设计思想

LongAdder 的核心思想:
“分而治之”,将一个变量分散成多个槽(Cell),不同线程更新不同的槽值,最终将所有槽的值求和得到结果。

  • 槽分散:通过内部的数组(cells[]),将变量分散到多个槽中。
  • 槽独立更新:线程会尽量更新一个独立的槽,减少竞争。
  • 最终求和:调用 sum() 方法时,将所有槽的值和基础值合并。

3. LongAdder 的实现原理

核心方法实现:

  1. 初始化和槽分散LongAdder 初始化时,只有一个基础值。高并发时,会动态初始化 cells[] 数组,将变量分散。
  2. 分槽更新: 线程尝试通过 CAS 更新某个槽的值。如果竞争失败,则尝试分配一个新的槽。
  3. 求和: 调用 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?

  1. 解决高并发性能瓶颈
    • 在高并发场景下,AtomicInteger 的性能下降明显,而 LongAdder 通过分槽的设计显著降低了线程争夺,提高了性能。
  2. 典型使用场景
    • 高并发场景中的计数器,例如:
      • 监控统计(如 QPS 计数器)。
      • 日志收集(如记录访问次数)。
  3. 对性能的优化
    • 在线程竞争激烈时,LongAdder 的吞吐量远高于 AtomicInteger

6. 总结

  • AtomicInteger:适合低并发场景,性能更简单,内存占用小。
  • LongAdder:适合高并发场景,通过分散竞争和分槽设计解决性能瓶颈,但代价是更高的内存开销。

发表评论

后才能评论