LongAdder 和 AtomicLong 的区别?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 原子类掌握度:面试官不仅仅是想知道这两个类 API 的区别,更是想知道你是否理解 AtomicLong 的 CAS 机制,以及它在高并发下的瓶颈在哪。

  2. 分段锁思想:考察你是否理解 LongAdder 用 "空间换时间" + "分散热点" 的思路来解决 CAS 竞争问题,这本质上是 ConcurrentHashMap 分段锁 思想的简化版。

  3. 选型能力:考察你能否根据实际场景(读多写少 vs 写多读少、是否需要精确的即时值)做出正确的技术选型,而不是无脑选 "性能更好" 的那个。

核心答案

一句话:AtomicLong 用一个 value 做 CAS,所有线程竞争同一个变量;LongAdder 把值分散到多个 Cell 中,每个线程只 CAS 自己的 Cell,最后求和。高并发写场景下 LongAdder 性能远超 AtomicLong

对比维度 AtomicLong LongAdder
底层原理 单变量 CAS 分散 Cell + CAS + 求和
高并发写性能 一般(热点竞争严重) 极好(分散竞争)
读取性能 O(1),直接返回 value 需遍历 Cell 求和
内存占用 少(1 个 value 多(Cell 数组)
适用场景 写入较少、需要精确即时读 高频写入、允许最终一致性读取
JDK 版本 JDK 5+ JDK 8+

深度解析

一、AtomicLong 的瓶颈在哪?

AtomicLong 的核心就是一个 volatile long value,每次 increment() 都是通过 CAS 更新这个唯一的变量:

// AtomicLong.incrementAndGet() 本质
public final long incrementAndGet() {
    // 无限循环 CAS
    while (true) {
        long current = get();
        long next = current + 1;
        if (compareAndSet(current, next)) {
            return next;
        }
        // CAS 失败,自旋重试
    }
}

这代码看着没毛病。但你想想,100 个线程同时 CAS 同一个变量,同一时刻只有一个能成功,剩下 99 个都得自旋重试。并发越高,CAS 失败率越高,重试越多,CPU 就在这空转。这就是典型的 伪共享(False Sharing) + 热点竞争 问题。

二、LongAdder 怎么解决的?

LongAdder 的思路特别巧妙——既然一个变量抢不过来,那就搞多个,大家各写各的,最后要结果的时候再求和。

上图展示了 LongAdder 的内部结构。核心机制拆解如下:

  • base:没有竞争时(通常是单线程),直接 CAS 写入 base,和 AtomicLong 一样
  • Cell[] 数组:一旦发生 CAS 竞争,就创建 Cell 数组。每个线程通过 Thread ID 哈希到不同的 Cell,各自 CAS 自己的 Cell,彻底消除竞争
  • sum() 求和:调用 .sum() 时遍历所有 Cell 求和:base + cells[0] + cells[1] + ...

这个设计思路和 ConcurrentHashMap 的分段锁异曲同工——把一把锁拆成多把锁,让冲突概率成倍下降。

三、Cell 的伪共享优化

这块是加分项。Cell 数组中每个 Cell 只有几个字节,但它们在内存中是连续排列的。而 CPU 缓存行一般是 64 字节,这就意味着 多个 Cell 可能落在同一个缓存行里。一个 Cell 被修改,整个缓存行失效,其他 Cell 跟着遭殃——这就是伪共享。

LongAdder 的解法很粗暴也很有效:给 Cell 加 @sun.misc.Contended 注解,让 JVM 自动在 Cell 两侧填充空白字节,确保每个 Cell 独占一个缓存行。

// LongAdder 内部 Cell 类(简化)
@sun.misc.Contended
static final class Cell {
    volatile long value;
    // ...
}

就这么一个注解,高并发下性能提升非常明显。JDK 中不少高性能并发类都用了这个技巧。

四、什么时候用谁?

这个很实际:

// 场景 1:接口 QPS 计数器 → LongAdder(高频写入,偶尔读取展示)
LongAdder requestCounter = new LongAdder();
requestCounter.increment();  // 请求进来 +1
long qps = requestCounter.sum();  // 每秒采样一次,求和

// 场景 2:序列号生成器 → AtomicLong(需要精确的唯一值,每次读取都要绝对准确)
AtomicLong idGenerator = new AtomicLong(0);
long nextId = idGenerator.incrementAndGet();  // 拿到的就是精确的递增值

简单说:高频累加计数用 LongAdder,需要精确的即时值用 AtomicLong

有个坑要注意:LongAdder.sum() 不是强一致的。你调用 .sum() 的时候,可能有线程正在往 Cell 里写,所以求和的结果和 "那一瞬间的真实值" 可能有微小偏差。对于计数器场景完全够用,但如果你要拿这个值做精确判断(比如限流判断),就得掂量掂量了。

面试高频追问

  1. LongAdder.sum() 为什么不是强一致的?

    因为 .sum() 求和的时候没有加锁,遍历 Cell 的过程中可能有其他线程正在更新 Cell。这就导致求和结果可能是 "过去某个时刻的近似值"。这是典型的 最终一致性 思路,用精度换性能。

  2. LongAccumulatorLongAdder 的关系?

    LongAdder 只支持加法运算(incrementadd),而 LongAccumulator 支持自定义的二元运算(加减乘除、maxmin 等)。LongAdder 本质上是 LongAccumulator 的一个特化版本。

  3. Cell 数组会无限扩容吗?

    不会。Cell 数组容量上限是 CPU 核心数(准确说是 2 的幂次方,最大不超过 NCPU)。因为最多也就这么多线程能真正并行执行,再多 Cell 也没用。

常见面试变体

  • "为什么高并发下 LongAdderAtomicLong 快?"
  • "LongAdder 的原理是什么?"
  • "什么是伪共享?LongAdder 是怎么解决的?"

记忆口诀

选型:高频累加用 LongAdder,精确即时用 AtomicLong

原理:一个变量抢不过,拆成多个各自写,最后求和就完事("分而治之,聚而求和"

总结

AtomicLongLongAdder 的本质区别在于:前者是 "单点 CAS",后者是 "分散 Cell + 求和"。LongAdder 用空间换时间,把热点竞争打散,在高并发写场景下性能碾压 AtomicLong。但 LongAdder.sum() 不是强一致的,需要精确值的场景还是得用 AtomicLong。面试时把 "分散热点 + 伪共享优化 + 最终一致性" 这三点讲清楚,这道题就稳了。