文章目录

Java DoubleAdder 为什么在高速并发下比 AtomicLong 吞吐量更高

发布于 2026-05-22 18:23:09 · 浏览 16 次 · 评论 0 条

Java DoubleAdder 为什么在高速并发下比 AtomicLong 吞吐量更高

在处理高并发计数场景时,选择正确的工具至关重要。AtomicLong 是 JDK 1.5 引入的经典并发计数器,而 DoubleAdder(及其兄弟 LongAdder)在 JDK 1.8 中被引入,旨在解决前者在高争用下的性能瓶颈。本文将直接剖析其设计原理,并给出明确的实操指导。


第一步:理解核心问题——单点锁与分段锁

首先,你需要明白两者性能差异的根本原因在于其内部数据结构的设计哲学。

  1. 描述 AtomicLong 的工作机制。它内部维护一个 volatile long value。当多个线程同时尝试更新时,它们都围绕这同一个内存地址(变量)进行操作。更新过程通常是一个 CAS(Compare-And-Swap) 循环:

    • 线程A读取当前值 current
    • 计算新值 current + delta
    • 尝试将 valuecurrent 原子地 更新为 new。如果更新失败(说明其他线程抢先修改了),则重试整个过程。
    • 关键点:所有线程都在竞争同一个“锁”(即 value 变量),在高并发下会产生大量CAS冲突和自旋等待,导致吞吐量下降。
  2. 描述 DoubleAdder 的工作机制。它采用了一种名为“分段”(Striped)的策略来减少竞争。

    • 它内部维护一个 volatile long base 值和一个 Cell[] 数组(每个 Cell 内部也是一个 volatile long value)。
    • 当线程尝试调用 add(double x) 方法时,它会尝试x 加到 base 上(通过CAS)。
    • 如果这次CAS成功,则直接返回,过程快速。
    • 如果CAS失败(说明 base 端有竞争),该线程会计算一个哈希值,用它来定位到 Cell[] 数组中的一个具体 Cell,然后将 x 加到这个 Cellvalue 上(同样使用CAS,但此时竞争范围缩小到了单个 Cell)。
    • 关键点:线程间的冲突被分散到了 base 和多个 Cell 上。高并发时,线程更可能分配到不同的 Cell,从而并行地完成更新,极大减少了整体竞争。最终的总和需要通过 sum() 方法汇总(base + 所有 Cell.value),这是一个“最终一致性”的操作。

第二步:通过代码直观感受差异

理论需要实践验证。下面我们分别编写使用 AtomicLongLongAdder(与 DoubleAdder 原理相同,用于计数更直观)的示例。

  1. 编写基础计数任务
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;

public class CounterBenchmark {
    // 模拟共享资源:两个计数器
    private static final AtomicLong atomicCounter = new AtomicLong(0);
    private static final LongAdder longAdder = new LongAdder();

    // 一个需要高并发执行的计数任务
    public static void incrementAtomic() {
        atomicCounter.incrementAndGet();
    }

    public static void incrementAdder() {
        longAdder.increment();
    }
}
  1. 创建高并发测试场景。我们创建一个测试类,模拟大量线程同时执行计数操作。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        final int threadCount = 64; // 并发线程数
        final int incrementsPerThread = 100_000; // 每个线程递增次数

        // 测试 AtomicLong
        testCounter(threadCount, incrementsPerThread, "AtomicLong", CounterBenchmark::incrementAtomic);
        System.out.println("AtomicLong final value: " + CounterBenchmark.atomicCounter.get());

        // 测试 LongAdder
        testCounter(threadCount, incrementsPerThread, "LongAdder", CounterBenchmark::incrementAdder);
        System.out.println("LongAdder final value: " + CounterBenchmark.longAdder.sum());
    }

    private static void testCounter(int threads, int iterations, String name, Runnable task) throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(threads);
        long startTime = System.currentTimeMillis();

        for (int i = 0; i < threads; i++) {
            executor.submit(() -> {
                for (int j = 0; j < iterations; j++) {
                    task.run();
                }
            });
        }

        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);
        long endTime = System.currentTimeMillis();
        System.out.printf("%s: %d operations in %d ms%n", name, (long) threads * iterations, (endTime - startTime));
    }
}
  1. 运行并观察结果。在大多数机器上,当线程数 (threadCount) 较高时,你会观察到 LongAdder 的执行时间显著低于 AtomicLong,但最终的 sum() 值与 AtomicLongget() 值相同。这直接证明了在高争用下,DoubleAdder/LongAdder 提供了更高的吞吐量

第三步:明确使用场景与选择策略

理解了原理和差异后,你需要知道在何种情况下选择哪种工具。

  1. 优先选择 AtomicLong 的场景

    • 需要严格的、实时的全局一致读。例如,当你需要随时通过 get() 方法获取一个精确的、线程安全的计数值时,AtomicLongget() 性能极好,只是简单地读取一个 volatile 变量。
    • 并发线程数很少。在低争用环境下,DoubleAdder 维护 Cell 数组的额外开销可能使其性能不如简单的 AtomicLong
    • 内存空间极度敏感DoubleAdder 会为每个线程(或哈希桶)分配一个 Cell 对象,占用更多内存。
  2. 优先选择 DoubleAdderLongAdder 的场景

    • 高并发写入为主的计数或统计场景。这是 DoubleAdder 的核心设计目标。例如,用于统计网站的页面点击量、API 调用次数、批量任务的处理数量等。调用 add() 方法的频率远高于 读取 sum() 的频率。
    • 可以容忍最终一致性读取。在计数过程中,sum() 返回的值可能是一个近似值(因为汇总时各 Cell 的值可能还在变化),但对于统计监控类应用,这个精度通常足够。
    • 追求更高的写入吞吐量。当系统面临每秒数十万甚至上百万的计数更新时,DoubleAdder 是更优的选择。

第四步:掌握关键方法与注意事项

在使用 DoubleAdder 时,牢记以下几点以确保正确性。

  1. 正确初始化。直接 创建 一个 DoubleAdder 对象。

    DoubleAdder adder = new DoubleAdder();
  2. 执行累加。在并发环境中 调用 add(double x) 方法。

    // 模拟多线程调用
    adder.add(1.0); // 累加1.0
    adder.add(2.5); // 累加2.5
  3. 获取近似总值调用 sum() 方法获取总和。请注意:此方法返回的是调用瞬间的一个快照,可能不是完全精确的实时值,但在统计上是合理的。

    double total = adder.sum(); // 返回约等于 3.5
  4. 重要注意事项

    • DoubleAdder 没有提供 compareAndSet()decrement() 等原子操作的组合方法。它设计用于简单的加法累加场景。如果需要复杂的原子更新逻辑(如“当值为X时,更新为Y”),你仍然需要使用 AtomicLong 或锁。
    • 不要 混用 add()reset() 方法,除非你完全理解这会导致计数器丢失在 reset() 调用之后、reset() 之前的所有累积值。reset() 仅适用于你明确知道需要清零的场景。

最终结论

DoubleAdder 通过“空间换时间”和“化整为零”的分段策略,将单个热点变量的争用压力分散到多个内部变量上,从而在高并发写入场景下实现了远超 AtomicLong 的吞吐量。选择哪个工具,最终取决于你的应用是更看重实时精确读,还是更看重极限写入性能。对于绝大多数面向吞吐量的计数统计需求,DoubleAdder(或 LongAdder)是更优解。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文