LongAdder 相比 AtomicXXX 类性能更好的原因是什么?请解释其工作原理。

参考回答**

LongAdder 相比 AtomicLong 和其他 AtomicXXX 类性能更好,主要是因为它采用了 分段累加(或称为分段锁)机制,减少了线程间的竞争。AtomicLong 在高并发的场景下,多个线程同时更新时会频繁地争用同一个变量,导致性能瓶颈。而 LongAdder 通过将更新操作分散到多个独立的槽中(即 Cell),每个线程更新不同的槽,从而显著减少了竞争,提高了并发性能。

工作原理

  1. LongAdder 内部维护了多个 Cell(独立的槽),每个槽都有一个独立的值。
  2. 线程在执行累加时会选择一个独立的槽进行操作,减少了多个线程竞争同一变量的情况。
  3. 最终获取结果时,会将所有 Cell 的值加总起来,得到最终的累加值。

详细讲解与拓展

1. 为什么 LongAdder 的性能优于 AtomicLong

AtomicLong 的局限性:

AtomicLong 使用 CAS(Compare-and-Swap) 操作来确保原子性。CAS 操作本身是线程安全的,但在高并发的情况下,多个线程可能会争抢对同一变量的访问,导致频繁的自旋和重试。这样,不仅浪费 CPU 时间,也会增加延迟,尤其是在多线程竞争非常激烈时,性能表现较差。

举个例子:假设有 1000 个线程同时对同一个 AtomicLong 变量进行累加操作。每次更新都需要使用 CAS 操作来判断当前值并更新。如果大量线程同时争用这个变量,会产生很多冲突,导致线程反复重试操作,性能会显著下降。

LongAdder 的优化:

LongAdder 通过 分段累加(将累加操作分配到多个独立的槽中)来避免线程间的竞争。内部维护一个 Cell[] 数组,每个 Cell 存储一个值。线程在执行 add 操作时,会选择一个槽进行累加,从而减少对同一 Cell 的竞争。

  1. 分段设计
    • LongAdder 将内部的累加操作分配到多个 Cell 中。每个线程可以对不同的 Cell 进行累加,而不会争用同一个内存位置。
    • 这样即使在高并发的情况下,每个 Cell 都有自己的局部更新,不会因为竞争同一个变量导致性能下降。
  2. 最终合并
    • 当获取累加结果时,LongAdder 会将所有 Cell 的值合并得到最终结果。

举个例子:在 1000 个线程并发执行时,每个线程会对 LongAdder 内部的不同 Cell 执行加法操作,而不是竞争同一个值。通过这种分段累加的方法,LongAdder 能够在高并发情况下提高性能。

2. LongAdder 的工作原理

LongAdder 内部通过 Cell[] 数组来存储多个 Cell,每个 Cell 都是一个独立的存储单元,用来存放累加值。线程在执行 add 操作时,会通过一些规则选择一个 Cell 来进行更新。最终,LongAdder 会将所有 Cell 的值加起来,得到最终的累加值。

具体步骤

  1. 初始化LongAdder 初始化时,会创建一个默认大小的 Cell[] 数组。
  2. 累加操作:每次累加时,LongAdder 会根据一些规则(如线程哈希值)选择一个 Cell 来执行 add 操作。
  3. 合并操作:获取总和时,会遍历 Cell[] 数组,将每个 Cell 的值加起来。

伪代码

public class LongAdder {
    private volatile Cell[] cells;

    public void add(long x) {
        // 获取 Cell 数组
        Cell[] cells = this.cells;
        // 选择一个 Cell 来进行操作
        Cell cell = chooseCell(cells);
        // 累加
        cell.add(x);
    }

    public long sum() {
        long sum = 0;
        // 遍历所有 Cell 累加
        for (Cell cell : cells) {
            sum += cell.value;
        }
        return sum;
    }
}
Java

3. LongAdderAtomicLong 的对比

特性 AtomicLong LongAdder
并发性能 高并发下可能产生较大性能瓶颈,因频繁竞争相同变量 高并发下能显著提高性能,避免了大量线程竞争同一个变量
适用场景 低并发、少量更新场景 高并发、大量更新场景
内存占用 使用一个单一的值进行存储 使用多个 Cell 存储累加值,内存占用稍大
累加操作 每次更新都通过 CAS 操作更新同一个变量 每个线程可能更新不同的槽,减少竞争
最终结果 获取值时直接读取变量的值 获取值时需要将所有 Cell 的值加起来

4. 示例代码

示例:AtomicLongLongAdder 的性能对比

import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;

public class PerformanceTest {
    private static final int NUM_THREADS = 1000;
    private static final int NUM_ITERATIONS = 1000;

    public static void main(String[] args) throws InterruptedException {
        // 使用 AtomicLong
        AtomicLong atomicLong = new AtomicLong(0);
        Thread[] atomicThreads = new Thread[NUM_THREADS];
        for (int i = 0; i < NUM_THREADS; i++) {
            atomicThreads[i] = new Thread(() -> {
                for (int j = 0; j < NUM_ITERATIONS; j++) {
                    atomicLong.incrementAndGet();
                }
            });
        }

        long startTime = System.nanoTime();
        for (Thread thread : atomicThreads) thread.start();
        for (Thread thread : atomicThreads) thread.join();
        long atomicDuration = System.nanoTime() - startTime;

        // 使用 LongAdder
        LongAdder longAdder = new LongAdder();
        Thread[] longAdderThreads = new Thread[NUM_THREADS];
        for (int i = 0; i < NUM_THREADS; i++) {
            longAdderThreads[i] = new Thread(() -> {
                for (int j = 0; j < NUM_ITERATIONS; j++) {
                    longAdder.add(1);
                }
            });
        }

        startTime = System.nanoTime();
        for (Thread thread : longAdderThreads) thread.start();
        for (Thread thread : longAdderThreads) thread.join();
        long longAdderDuration = System.nanoTime() - startTime;

        System.out.println("AtomicLong duration: " + atomicDuration + " ns");
        System.out.println("LongAdder duration: " + longAdderDuration + " ns");
    }
}
Java

5. 总结

  • LongAdder 的优势:
    • 通过 分段累加,减少了多个线程竞争同一个变量的情况,极大提高了高并发情况下的性能。
    • 适用于需要频繁累加的高并发场景。
  • AtomicLong
    • 适用于低并发或更新较少的场景,适合单一变量的原子更新。
    • 在高并发情况下,由于 CAS 的竞争,可能会造成性能瓶颈。

发表评论

后才能评论