导读:本期聚焦于小伙伴创作的《如何通过 LongAdder 的 Cell 数组分段累加机制解决高并发下的原子计数器性能瓶颈》,敬请观看详情。在每秒数十万次递增的请求场景下,AtomicLong 依靠单个 volatile 变量的 CAS 操作会产生严重的线程竞争,导致大量线程自旋重试。LongAdder 采用 Cell 数组将计数拆为多段,各线程优先更新自己对应的 Cell,最终汇总 base 与所有 Cell 得到总值。这种分段累加思路把热点数据分散到不同内存位置,降低了缓存行争用。理解 Cell 的惰性初始化、扩容条件以及 sum 方法的最终一致性,能帮助我们在统计接口流量、限流计数等场景中避开性能陡降的坑。

在构建高并发系统时,计数器是最基础也最容易成为瓶颈的组件。当多个线程同时对同一个原子变量做递增操作时,传统方案往往因为激烈的 CAS 竞争而让吞吐量急剧下滑。LongAdder 通过引入 Cell 数组分段累加的机制,把写压力分散开来,从而显著提升高并发下的计数性能。

如何通过 LongAdder 的 Cell 数组分段累加机制解决高并发下的原子计数器性能瓶颈

一、AtomicLong 的性能瓶颈在哪里

AtomicLong 内部维护了一个 volatile 修饰的 long 型变量 value,所有自增操作都通过 Unsafe 的 compareAndSwapLong 方法完成。在单线程或低并发环境中,这种实现简单且高效。但在高并发场景中,成百上千的线程会同时尝试修改同一个 value,只有一个线程能成功,其余线程只能循环重试。

这种重试在 CPU 层面表现为大量的缓存一致性流量。由于 value 所在的内存地址被所有核同时争抢,MESI 协议会频繁触发缓存行失效,导致总线风暴。下面的代码展示了 AtomicLong 的典型用法,它在低并发下没有问题,但在压测中很容易看到线程等待时间飙升。

import java.util.concurrent.atomic.AtomicLong;

public class CounterDemo {
    private static final AtomicLong counter = new AtomicLong(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 100000; i++) {
                counter.incrementAndGet();
            }
        };
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("final count=" + counter.get());
    }
}

上述代码在双线程各十万次递增时可能还能接受,但当线程数上升到几十甚至上百时,incrementAndGet 的耗时会出现数量级增长。瓶颈并不在 Java 代码本身,而在底层硬件对共享变量的访问冲突。

二、LongAdder 的 Cell 数组分段思想

LongAdder 的核心思路是:不要求所有线程修改同一个变量,而是让线程分散到多个称为 Cell 的存储单元上。每个 Cell 本质上也是一个填充过的 volatile long 变量,被包装在 @Contended 注解的类中以减少伪共享。LongAdder 自身除了一个 base 变量外,还持有一个 Cell 数组。

当线程进行 add 操作时,LongAdder 会先根据线程的探针哈希(threadLocalRandomProbe)计算出一个 Cell 下标。如果该下标位置的 Cell 已存在,就尝试对它做 CAS 累加;如果数组尚未初始化,或者 CAS 失败发生竞争,才会去扩容数组或回退到 base 上操作。这种机制让不同的线程大概率操作不同的内存地址,从而把热点打散。

import java.util.concurrent.atomic.LongAdder;

public class LongAdderDemo {
    private static final LongAdder adder = new LongAdder();

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 100000; i++) {
                adder.increment();
            }
        };
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(task);
            threads[i].start();
        }
        for (Thread t : threads) {
            t.join();
        }
        // sum 会累加 base 与所有 Cell 的值
        System.out.println("final sum=" + adder.sum());
    }
}

从代码可以看出,使用方式几乎和 AtomicLong 一样简单,但内部已经完全改变。Cell 数组的长度总是 2 的幂,方便用位运算定位,同时在扩容时也会尽量保持线程映射的均衡。

三、Cell 的惰性初始化与扩容机制

LongAdder 并不会在一开始就创建 Cell 数组。刚实例化时,所有累加都落在 base 上,此时表现和 AtomicLong 类似。只有当发生第一次 CAS 竞争,例如两个线程同时更新 base 失败,LongAdder 才会初始化长度为 2 的 Cell 数组,并把竞争线程引导到不同 Cell。

如果某个 Cell 上的 CAS 依然失败,说明该分段也出现了热点,此时会触发扩容:数组长度翻倍,并将原有 Cell 重新分配到新位置。扩容过程借助总线锁或 CAS 控制并发,保证同一时刻只有一个线程能修改 cells 引用。下面的简化逻辑说明了扩容判断的关键点:

// 伪代码:说明 LongAdder 内部扩容思路
if (cells == null) {
    // 惰性初始化
    cells = new Cell[2];
} else if (cells.length <= 当前竞争阈值) {
    // 发生竞争,扩容为两倍
    Cell[] newCells = new Cell[cells.length * 2];
    System.arraycopy(cells, 0, newCells, 0, cells.length);
    cells = newCells;
}

值得注意的是,Cell 类使用了缓存行填充,避免在多核 CPU 上因伪共享导致相邻 Cell 互相 invalidate。JDK 8 之后通过 @sun.misc.Contended 注解实现,运行时需开启相应参数才能生效。这也解释了为什么 LongAdder 在空间上比 AtomicLong 占用更多内存,但换来了写性能的数量级提升。

四、sum 方法的最终一致性与使用注意

由于累加被分散到 base 和多个 Cell 中,LongAdder 的 sum 方法在调用瞬间会把它们全部加起来。这个过程没有全局锁,因此在并发仍在进行的场景下,sum 返回的是一个近似值,而非实时精确值。这就是所谓的最终一致性。

对于绝大多数监控、限流、统计场景,短暂的数值偏差完全可以接受。但如果业务要求绝对精确的实时计数,例如金融账户余额,就不能使用 LongAdder,而应继续使用 AtomicLong 或加锁方案。此外,调用 sum 之后如果不再写入,数值便会稳定,因此很多系统会在分钟级定时采集 adder.sumThenReset() 来获取区间增量并清零。

对比维度AtomicLongLongAdder
竞争模型单变量 CASCell 数组分段 CAS
高并发写性能随线程数增加陡降接近线性扩展
读取实时性精确最终一致
内存占用较高

上表总结了两者差异。可以看到,LongAdder 用空间和弱一致性换取了高并发下的吞吐能力,这正是分段累加机制的价值所在。

五、在实际项目中落地分段计数

在网关层统计每接口 QPS 时,通常会为每个接口名维护一个 LongAdder。请求进入时调用 increment,后台定时任务每秒拉取 sum 并重置,既避免了锁开销,也能支撑海量并发。如果担心 @Contended 未生效,可以显式在启动参数加入 -XX:-RestrictContended 并确认 JDK 版本支持。

当计数器数量极多且访问极分散时,也可以考虑将多个 LongAdder 按业务维度分片,进一步降低单个 adder 的竞争概率。总之,理解 Cell 数组如何把原子操作从集中走向分布,是写出高吞吐并发代码的重要一步。

LongAdderCell数组高并发计数器修改时间:2026-08-06 07:24:31

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。