在开发多线程应用时,你是否曾遇到这样的困扰:随着并发量增加,系统性能不升反降?特别是在计数器场景下,本应简单的自增操作却成了性能瓶颈。这正是许多 Java 开发者共同面临的痛点。当线程数超过 CPU 核心数或竞争激烈时,AtomicLong 的 CAS 操作不断失败重试,CPU 使用率飙升,而业务处理效率却直线下降。这也是为什么阿里巴巴在其开发规范中明确推荐使用 LongAdder 来替代传统方案。

LongAdder 是什么

LongAdder 是 Java 8 在java.util.concurrent.atomic包中引入的一个新的原子性操作类,专为高并发环境下的计数场景设计。与传统的 AtomicLong 相比,它采用了更加优化的内部实现,能够有效减少线程间的竞争,提高并发性能。

graph TD
    A[JUC原子类] --> B[基本类型原子类]
    A --> C[数组原子类]
    A --> D[引用原子类]
    A --> E[累加器]
    E --> F[LongAdder]
    E --> G[DoubleAdder]
    B --> H[AtomicLong]

为什么需要 LongAdder

在分析 LongAdder 的优势前,我们先了解传统方案 AtomicLong 的问题:

AtomicLong 的性能瓶颈

AtomicLong 主要依赖 CAS(Compare-And-Swap)操作保证原子性。当多线程同时更新同一个计数器时,会出现以下问题:

  1. 激烈的竞争:所有线程竞争同一个值的更新权
  2. 频繁的失败和重试:CAS 操作在高并发下失败率高
  3. CPU 空转:频繁 CAS 重试导致 CPU 资源浪费
  4. CAS 竞争热点:多线程争抢同一变量更新权,形成系统瓶颈

下面是 AtomicLong 的主要更新方法实现:

public final long incrementAndGet() {
    return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}

这种实现在高并发下会导致大量线程自旋等待,形成"CAS 竞争热点"问题。

sequenceDiagram
    participant 线程1
    participant 线程2
    participant 线程3
    participant AtomicLong

    线程1->>AtomicLong: 尝试CAS(期望值:0, 新值:1)
    AtomicLong-->>线程1: 成功(当前值:1)
    线程2->>AtomicLong: 尝试CAS(期望值:0, 新值:1)
    AtomicLong-->>线程2: 失败(当前值:1)
    线程2->>AtomicLong: 重试CAS(期望值:1, 新值:2)
    AtomicLong-->>线程2: 成功(当前值:2)
    线程3->>AtomicLong: 尝试CAS(期望值:0, 新值:1)
    AtomicLong-->>线程3: 失败(当前值:2)
    线程3->>AtomicLong: 重试CAS(期望值:2, 新值:3)
    AtomicLong-->>线程3: 成功(当前值:3)

LongAdder 的实现原理

LongAdder 采用了"分段累加"的设计思想,巧妙地避开了 AtomicLong 面临的竞争问题。

核心设计:分段计数

LongAdder 内部维护了一个基础值 base 和一个 Cell 数组。这里可以把它想象成一个"分布式计数器":

graph LR
    A[LongAdder] --> B[base值]
    A --> C[Cell数组]
    C --> D["Cell[0]"]
    C --> E["Cell[1]"]
    C --> F["Cell[2]"]
    C --> G["Cell[...]"]

    线程1 -.->|无竞争时| B
    线程2 -.->|"线程哈希 & (n-1) = 0"| D
    线程3 -.->|"线程哈希 & (n-1) = 1"| E
    线程4 -.->|"线程哈希 & (n-1) = 2"| F

    style 线程1 fill:#f9f,stroke:#333
    style 线程2 fill:#bbf,stroke:#333
    style 线程3 fill:#bfb,stroke:#333
    style 线程4 fill:#fbb,stroke:#333

线程哈希计算伪代码:

线程哈希 = ThreadLocalRandom.getProbe();
cellIndex = 线程哈希 & (cells.length - 1);  // 位运算提高效率

工作流程如下:

  1. 无竞争时很简单:所有线程都更新 base 值,性能接近 AtomicLong
  2. 出现竞争后动态分流:不同线程被分配到不同的 Cell 格子去更新,互不干扰
  3. 结果计算设计简洁:base 值加上所有 Cell 值的总和

这种设计就像银行柜台:人少时一个窗口就够了(base);人多时开放多个窗口(Cells),每个客户去不同窗口办理,互不影响,效率大大提高。

内存占用对比

LongAdder 采用了空间换时间的设计思路,下面是 AtomicLong 与 LongAdder 的内存占用对比:

graph LR
    subgraph 内存占用对比
        A[AtomicLong] --> B[value变量: 8字节]
        A --> C[对象头: 16字节]
        D[LongAdder] --> E[base: 8字节]
        D --> F["cells数组: N×128字节(每个Cell占1个缓存行)"]
    end

Cell 类与伪共享问题

Cell 类是 LongAdder 内部的核心组件,它使用了@Contended注解避免伪共享问题:

@sun.misc.Contended
static final class Cell {
    volatile long value;
    Cell(long x) { value = x; }
    // CAS更新方法
    final boolean cas(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
    }
}

伪共享(False Sharing):当多个线程频繁修改位于同一 CPU 缓存行的不同变量时,即使变量无关,也会因缓存一致性协议导致缓存行频繁失效,降低性能。@Contended通过填充字节使Cell实例独占缓存行,避免此问题。值得注意的是,在 OpenJDK 中,@Contended注解默认仅对 JDK 内部类生效,外部应用需通过-XX:-RestrictContended参数启用。

这就像在超市购物:如果相邻的收银台共用一个出口通道,一个收银台的顾客在结账会影响另一个收银台顾客的通行。@Contended注解相当于给每个收银台都建立了独立的出口通道,避免了这种无谓的等待。

动态扩容机制

LongAdder 不会一开始就创建很多 Cell,而是按需增长:

  1. 初始状态节省内存:只有一个 base 值,所有线程都往这里加数
  2. 竞争时才扩容:当发现 base 更新冲突,才初始化 Cell 数组(初始大小为 2)
  3. 持续优化分配:线程通过ThreadLocalRandom.getProbe()生成哈希值映射到对应 Cell

当 Cell 更新失败时,longAccumulate方法会:

  1. 重试更新:通过ThreadLocalRandom.advanceProbe()生成新的哈希值,避持续冲突
  2. 扩容 cells 数组:若重试多次失败,且 cells 长度小于2^24,则将数组长度翻倍(2→4→8),扩容上限为2^24,以避免无限制增长
  3. 处理极端情况:当扩容后仍冲突,或系统资源紧张时,会通过自旋(Thread.yield())或短暂休眠减少 CPU 占用,避免活锁

需要注意的是,即使在最理想的情况下,当线程数极多时(如接近2^24),或者哈希冲突严重时,LongAdder 也可能退化为类似 AtomicLong 的竞争模式,只是概率大大降低。

关键代码解析

LongAdder 的核心方法 add()实现(简化版):

public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    // 如果Cells数组已初始化 或者 更新base值失败(说明有竞争)
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true;
        // 如果Cells数组未初始化 或 当前线程的Cell未初始化 或 更新Cell失败
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[getProbe() & m]) == null ||
            !(uncontended = a.cas(v = a.value, v + x)))
            longAccumulate(x, null, uncontended);
    }
}

sum()方法获取最终值:

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

sum()方法本身是线程安全的(不需要额外同步),但呈现的是弱一致性结果,它在读取时不会阻塞写操作,可能读到部分更新的中间状态(如某Cell正在被更新时读取其旧值)。这与 AtomicLong 的get()方法提供的强一致性形成对比。但当所有写操作完成后,多次调用sum()结果会一致(最终一致性),这对统计类场景(如 QPS、总量计数)已经足够。

工作原理图解

sequenceDiagram
    participant 线程1
    participant 线程2
    participant 线程3
    participant LongAdder

    线程1->>LongAdder: add(1)
    LongAdder-->>LongAdder: 尝试更新base
    LongAdder-->>线程1: 成功(base=1)

    线程2->>LongAdder: add(1)
    LongAdder-->>LongAdder: 尝试更新base(失败,竞争)
    LongAdder-->>LongAdder: 初始化cells数组
    LongAdder-->>LongAdder: 更新Cell[0]=1
    LongAdder-->>线程2: 成功

    线程3->>LongAdder: add(1)
    LongAdder-->>LongAdder: 尝试更新base(已有cells数组)
    LongAdder-->>LongAdder: 通过线程哈希映射到Cell[1]
    LongAdder-->>LongAdder: 更新Cell[1]=1
    LongAdder-->>线程3: 成功

    Note over LongAdder: 最终结果 = base(1) + Cell[0](1) + Cell[1](1) = 3

性能测试与对比

下面通过一个简单的性能测试,对比 LongAdder 和 AtomicLong:

import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

public class CounterPerformanceTest {
    private static final int ITERATIONS = 100000;

    public static void main(String[] args) throws Exception {
        // 测试不同线程数下的性能
        for (int threadCount : new int[]{10, 100, 500, 1000, 2000}) {
            System.out.println("测试线程数: " + threadCount);
            testAtomicLong(threadCount);
            testLongAdder(threadCount);
            System.out.println();
        }
    }

    private static void testAtomicLong(int threadCount) throws Exception {
        final AtomicLong counter = new AtomicLong(0);
        long start = System.currentTimeMillis();

        ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
        for (int i = 0; i < threadCount; i++) {
            executorService.submit(() -> {
                for (int j = 0; j < ITERATIONS; j++) {
                    counter.incrementAndGet();
                }
            });
        }

        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.HOURS);

        long end = System.currentTimeMillis();
        System.out.println("AtomicLong: " + (end - start) + "ms, Result: " + counter.get());
    }

    private static void testLongAdder(int threadCount) throws Exception {
        final LongAdder counter = new LongAdder();
        long start = System.currentTimeMillis();

        ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
        for (int i = 0; i < threadCount; i++) {
            executorService.submit(() -> {
                for (int j = 0; j < ITERATIONS; j++) {
                    counter.increment();
                }
            });
        }

        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.HOURS);

        long end = System.currentTimeMillis();
        System.out.println("LongAdder: " + (end - start) + "ms, Result: " + counter.sum());
    }
}

测试结果

测试环境:Intel i7-10700K(8 核 16 线程),JDK 11.0.12,OpenJDK 64 位,内存 16GB

可以看到几个有趣的现象:

  1. 在低并发(10 线程)时,AtomicLong 甚至略快于 LongAdder,这是因为 LongAdder 有额外的判断逻辑
  2. 随着线程数的增加,AtomicLong 性能直线下降,而 LongAdder 的性能下降相对平缓
  3. 在 2000 线程的场景下,在测试环境中 LongAdder 的性能约为 AtomicLong 的 7 倍

这就像高峰时段的马路:单车道会越来越堵,而多车道则能保持较高的通行效率。但在车辆稀少时,单车道反而更简单高效。

性能差异原因分析

  1. 减少争用:LongAdder 通过分散更新不同的 Cell,大大减少了线程间的竞争
  2. 降低失败率:每个线程更可能操作不同的 Cell,CAS 操作成功率更高
  3. 提高并行度:多个线程可以并行更新不同的计数单元,而不是串行等待
  4. 避免伪共享:Cell 类使用了@Contended 注解,避免了缓存行伪共享问题

实际应用场景

LongAdder 特别适合以下场景:

  1. 高并发计数:如系统运行状态监控、QPS 统计
  2. 性能指标收集:统计接口调用次数、成功率等
  3. 限流计数器:短时间内的请求量统计
  4. 缓存命中率统计:记录缓存命中和未命中次数

选择正确的计数器

根据不同场景选择合适的计数器工具:

// 场景1: 需要原子条件更新的场景 - 使用AtomicLong
AtomicLong sequencer = new AtomicLong(0);
// 生成下一个序列号,同时确保不超过最大值
long nextId = sequencer.updateAndGet(current ->
    current < MAX_SEQUENCE ? current + 1 : current);

// 场景2: 高并发计数统计场景 - 使用LongAdder
LongAdder totalRequests = new LongAdder();
// 多线程并发调用
totalRequests.increment();
// 定时任务汇总打印
scheduler.scheduleAtFixedRate(() -> {
    System.out.println("当前总请求数: " + totalRequests.sum());
}, 0, 5, TimeUnit.SECONDS);

应用示例:接口监控计数器

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LongAdder;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;

public class ApiMetricsCounter {
    private final ConcurrentHashMap<String, LongAdder> apiCounters = new ConcurrentHashMap<>();

    public void recordApiCall(String apiName) {
        // 获取或创建对应API的计数器
        apiCounters.computeIfAbsent(apiName, k -> new LongAdder()).increment();
    }

    public long getApiCount(String apiName) {
        LongAdder counter = apiCounters.get(apiName);
        return counter == null ? 0 : counter.sum();
    }

    public void printAllMetrics() {
        apiCounters.forEach((api, counter) ->
            System.out.println(api + ": " + counter.sum() + " calls"));
    }

    // 使用示例
    public static void main(String[] args) throws Exception {
        ApiMetricsCounter metrics = new ApiMetricsCounter();

        // 模拟多线程调用
        ExecutorService executor = Executors.newFixedThreadPool(100);
        for (int i = 0; i < 1000; i++) {
            final int index = i % 3;
            executor.submit(() -> {
                String api = "api" + index;
                metrics.recordApiCall(api);
            });
        }

        executor.shutdown();
        executor.awaitTermination(10, TimeUnit.SECONDS);

        // 打印结果
        metrics.printAllMetrics();
    }
}

生产环境监控集成

在实际的生产环境中,可以使用 Micrometer 等监控框架来采集 LongAdder 的数据,避免频繁调用sum()

// 使用Micrometer监控LongAdder
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.FunctionCounter;

public class MetricsService {
    private final LongAdder requestCounter = new LongAdder();

    public MetricsService(MeterRegistry registry) {
        // 注册LongAdder到Micrometer,自动定期采集数据
        FunctionCounter.builder("api.requests", requestCounter, LongAdder::sum)
            .description("API请求总数")
            .register(registry);
    }

    public void recordRequest() {
        requestCounter.increment();
    }
}

LongAdder 的注意事项

虽然 LongAdder 性能优越,但也有一些使用注意事项:

  1. 内存占用:LongAdder 内部的 Cell 数组会占用更多内存。AtomicLong 只有一个 value 变量(约 24 字节),而 LongAdder 的每个 Cell 因为@Contended 注解占用约 128 字节(一个缓存行大小)。就像高速公路:单车道省地但容易堵,多车道通行效率高但占地多。
  2. 非精确读取:在高并发更新时调用sum(),结果可能不是实时准确的,因为求和过程中可能有新的更新发生。这就像统计进出商场的人数,你在数的过程中可能有人进出,导致结果有轻微偏差。
  3. 哈希冲突:当线程数远超 Cell 数组大小时,可能多个线程映射到同一个 Cell,造成局部热点。以下是一个简单的工具,帮助你检查 Cell 数组状态:
// 调试用:查看LongAdder的Cell分布情况
// 注意:反射调用非公开API,可能导致兼容性问题,仅用于调试分析
private static void checkCellDistribution(LongAdder adder) {
    try {
        // 反射获取cells字段
        Field cellsField = LongAdder.class.getDeclaredField("cells");
        cellsField.setAccessible(true);
        Object[] cells = (Object[]) cellsField.get(adder);

        if (cells == null) {
            System.out.println("cells数组未初始化,所有线程更新base值");
            return;
        }

        System.out.println("Cell数组大小: " + cells.length);
        for (int i = 0; i < cells.length; i++) {
            if (cells[i] != null) {
                Field valueField = cells[i].getClass().getDeclaredField("value");
                valueField.setAccessible(true);
                long value = (long) valueField.get(cells[i]);
                System.out.println("Cell[" + i + "] 值: " + value);
            } else {
                System.out.println("Cell[" + i + "] 未创建");
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

性能优化建议

若通过调试工具发现某Cell竞争激烈(如Cell[0]值远高于其他Cell),可通过 JVM 参数预设初始cells大小:

-Djava.util.concurrent.atomic.LongAdder.cellSize=32

此参数需谨慎使用,默认初始cellSize为 2,扩容策略与线程竞争程度相关。通常不建议设置超过 CPU 核心数的 2-4 倍,盲目增大可能导致内存浪费,建议仅在确认存在严重哈希冲突时调整。

sum()调用非常频繁时(如高频监控),可考虑使用sumThenReset()方法获取并重置计数器,减少多次累加的开销:

long total = counter.sumThenReset(); // 获取当前总数并清零

实际应用建议

基于以上分析,在实际开发中可以遵循以下建议:

  1. 在高并发统计场景(写多读少)下,优先使用 LongAdder 而非 AtomicLong
  2. 对于计数器类场景(如统计、度量),使用 LongAdder 能带来显著性能提升
  3. 需要精确原子操作(如序列号生成)的场景,仍然使用 AtomicLong
  4. 低并发场景下,两者性能差异不大,可以根据实际需求选择

总结

特性AtomicLongLongAdder
并发性能较低,高并发下性能下降明显优秀,线程数越多优势越明显
内存占用低(约 24 字节)较高(base + N 个 Cell,每个 Cell 约 128 字节)
精确性实时精确(get()强一致)最终一致(sum()允许短暂不一致)
适用场景需要原子条件更新统计类场景(高并发计数)
实现复杂度简单,基于单个变量的 CAS 操作复杂,涉及 base、Cell 数组、动态扩容、哈希映射等
Java 版本Java 5+Java 8+
低并发表现性能略好(实现简单)略有额外开销(判断逻辑)

LongAdder 通过巧妙的分段设计,有效解决了高并发下 AtomicLong 的性能瓶颈。这也是阿里巴巴在 Java 开发手册中推荐使用 LongAdder 的主要原因。

在实际开发中,我们应根据应用场景选择合适的工具。就像选择交通工具:短途可以骑自行车(AtomicLong 简单够用),长途拥堵路段就需要高铁(LongAdder 突破瓶颈)。对于监控、统计类高并发场景,LongAdder 通常是更优选择;而对于需要精确原子操作的场景,AtomicLong 仍然是必要的。


异常君
1 声望2 粉丝

在 Java 的世界里,永远有下一座技术高峰等着你。我愿做你登山路上的同频伙伴,陪你从看懂代码到写出让自己骄傲的代码。咱们,代码里见!