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;
    }
}

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");
    }
}

5. 总结

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

发表评论

后才能评论