侧边栏壁纸
博主头像
牧云

怀璧慎显,博识谨言。

  • 累计撰写 198 篇文章
  • 累计创建 19 个标签
  • 累计收到 8 条评论

目 录CONTENT

文章目录

深入解析 Java 高并发计数器:LongAdder vs AtomicLong

秋之牧云
2026-05-14 / 0 评论 / 0 点赞 / 0 阅读 / 0 字

在 Java 并发编程中,线程安全的计数操作是极其常见的需求。JDK 提供了 AtomicLongLongAdder 两种主要工具。虽然它们都能实现原子计数,但在高并发场景下的表现却天差地别。本文将深入探讨两者的底层原理、性能差异及最佳实践。

1. 核心区别概览

简单来说,AtomicLong 是所有线程竞争同一个变量进行 CAS(Compare-And-Swap)更新;而 LongAdder 则将值分散到多个单元格(Cell)中,每个线程只更新自己对应的 Cell,最后通过求和得到总值。

特性AtomicLongLongAdder
底层机制单变量 CAS 自旋分段计数(Cell 数组)+ 最终求和
高并发写性能较差(热点竞争严重)极佳(分散竞争,吞吐量高)
内存占用低(仅 1 个 volatile long)高(维护 Cell 数组,空间换时间)
数据一致性强一致,支持原子读-改-写最终一致性sum() 非原子快照
适用场景低并发、需精确即时值、序列号生成高频写入、统计监控、写多读少

2. AtomicLong 的性能瓶颈

AtomicLong 的核心是一个 volatile long value。其递增操作如下:

public final long incrementAndGet() {
    while (true) {
        long current = get();
        long next = current + 1;
        if (compareAndSet(current, next)) {
            return next;
        }
        // CAS 失败,继续自旋重试
    }
}

在低并发下,这种机制非常高效。然而,当大量线程同时尝试修改同一个变量时:

  1. 热点竞争:同一时刻只有一个线程能 CAS 成功。
  2. CPU 空转:失败的线程进入 while(true) 循环不断重试,消耗大量 CPU 资源却不产生有效工作。
  3. 性能天花板:随着线程数增加,CAS 失败率急剧上升,吞吐量不再增长甚至下降。

3. LongAdder 的设计思想:分散热点

LongAdder 借鉴了 ConcurrentHashMap 的分段锁思想,采用 “空间换时间”“分散热点” 的策略来解决竞争问题。

3.1 内部结构

LongAdder 内部维护两个部分:

  • base:基础值。在无竞争或低并发时,直接通过 CAS 更新 base,行为类似 AtomicLong。
  • Cell[] 数组:当检测到竞争时,初始化 Cell 数组。每个 Cell 也是一个简单的计数器。

3.2 工作流程

  1. 无竞争时:线程直接 CAS 更新 base
  2. 有竞争时
    • 系统根据线程的哈希值(通常基于 ThreadLocalRandom.getProbe()),将线程映射到 Cell 数组中的一个特定槽位。
    • 线程只对自己映射到的那个 Cell 进行 CAS 更新。
    • 由于不同线程大概率映射到不同的 Cell,竞争被大幅分散,CAS 成功率显著提高。
  3. 求和时:调用 sum() 方法,遍历所有 Cell 并将它们的值与 base 累加:result = base + cell[0] + cell[1] + ...

3.3 消除伪共享(False Sharing)

这是 LongAdder 高性能的关键细节之一。

  • 问题:CPU 缓存以“缓存行”(Cache Line,通常 64 字节)为单位加载。如果多个 Cell 对象位于同一个缓存行中,一个线程修改某个 Cell 会导致整个缓存行失效,其他访问相邻 Cell 的线程必须重新从内存加载数据,导致性能下降。
  • 解决:LongAdder 内部的 Cell 类使用了 @sun.misc.Contended 注解。该注解指示 JVM 在 Cell 对象前后填充空白字节,确保每个 Cell 独占一个缓存行,从而彻底消除伪共享带来的性能损耗。

4. 关键注意事项:最终一致性

LongAdder 的 sum() 方法不是原子操作

  • 在遍历 Cell 数组求和的过程中,其他线程可能正在更新某些 Cell 的值。
  • 因此,sum() 返回的是当前时刻的近似值,而非严格的瞬时快照。
  • 对于监控统计(如 QPS、PV)等场景,这种微小的误差完全可以接受。
  • 警告:如果你需要基于当前计数值做出严格的业务决策(如精确限流、金融交易扣减),不能使用 LongAdder,应选用 AtomicLong 或加锁机制。

5. 选型指南:什么时候用谁?

✅ 选择 LongAdder 的场景

  • 高并发计数统计:如 Web 服务的 QPS/TPS 统计、接口调用次数、错误日志计数。
  • 写多读少:更新操作极其频繁,而读取总和的操作相对稀疏(例如每秒上报一次监控数据)。
  • 对实时精度要求不高:允许数据存在短暂的不一致,追求极致的写入吞吐量。
// 示例:高并发请求计数
LongAdder counter = new LongAdder();

public void handleRequest() {
    counter.increment(); // 高性能累加
    // ... 业务逻辑
}

public long getQPS() {
    return counter.sum(); // 低频读取,获取近似总和
}

✅ 选择 AtomicLong 的场景

  • 需要精确的即时值:如生成全局唯一序列号、订单号。
  • 原子性读-改-写:需要基于当前值进行判断并更新,例如 compareAndSetgetAndIncrement(需要返回旧值用于逻辑判断)。
  • 低并发或读写均衡:竞争不激烈时,AtomicLong 内存占用更小,代码更简单,且无额外开销。
// 示例:生成唯一 ID
AtomicLong idGenerator = new AtomicLong(0);

public long getNextId() {
    // 必须保证返回的值是唯一的、递增的
    return idGenerator.incrementAndGet(); 
}

6. 总结

  • AtomicLong 适用于低竞争或需要强一致性、原子性返回值的场景。它是“单点突破”,简单直接。
  • LongAdder 适用于高并发、写多读少的统计场景。它通过分段计数消除伪共享技术,以少量的内存开销换取了巨大的性能提升。

在实际开发中,如果是做监控埋点、流量统计,请优先使用 LongAdder;如果是做业务逻辑控制、ID 生成,请坚持使用 AtomicLong。理解两者的本质区别,有助于我们在高并发系统中做出更合理的性能优化决策。

0

评论区