第三章 无锁数据结构:从AtomicInteger到LongAdder

说到Java里的无锁原语,我最早接触的是AtomicInteger。那时候刚做交易系统,心里想:这玩意儿比synchronized快多了吧?后来踩了坑才发现——没那么简单。

今天咱们就聊聊,从AtomicIntegerLongAdder,Java到底经历了什么。说白了,就是一场「锁竞争消除」的进化史。

3.1 AtomicInteger:CAS的起点

AtomicInteger的核心是CAS(Compare-And-Swap)。它用CPU的硬件指令保证原子性,不需要操作系统挂起线程。嗯,听起来很美。

但我在项目中遇到过一个问题:高并发下,AtomicInteger的性能会急剧下降。为什么会这样?

你想想看,CAS是乐观锁。它假设冲突很少,直接尝试更新。如果失败了,就自旋重试。但一旦线程数多了,大家都抢同一个变量——

核心问题:CAS自旋会导致大量CPU空转,这叫「忙等待」。线程越多,自旋次数越多,性能反而越差。

我举个例子:

// 高并发下的AtomicInteger
AtomicInteger counter = new AtomicInteger(0);

// 100个线程同时执行
for (int i = 0; i < 100; i++) {
    new Thread(() -> {
        for (int j = 0; j < 10000; j++) {
            counter.incrementAndGet(); // CAS自旋
        }
    }).start();
}

这段代码跑起来,你会发现CPU占用率飙升。每个线程都在拼命重试,但真正成功的只有一个。其他99个都在浪费电。

我曾经踩过的坑:在交易系统的订单计数器里用了AtomicInteger,压测到5000TPS时,CPU直接飙到95%。后来换成LongAdder,同样压力下CPU降到30%。差距就是这么明显。

3.2 LongAdder:分而治之的艺术

LongAdder是Java 8引入的。它的思路很简单:把竞争分散。

怎么分散?

  • 内部维护一个Cell[]数组
  • 每个线程映射到不同的Cell
  • 更新时只操作自己的Cell
  • 取值时把所有Cell的值加起来

说白了,就是把一个热点变量,拆成多个冷门变量。每个线程各玩各的,互不干扰。

个人习惯:只要是多线程统计场景,我首选LongAdder。比如交易系统的成交笔数、委托笔数、延迟统计——这些高频写入、低频读取的场景,LongAdder简直是神器。

看个对比:

特性 AtomicInteger LongAdder
底层机制 CAS自旋 Cell数组 + CAS
高并发写入 性能急剧下降 线性扩展
读取性能 快(直接取值) 慢(需要求和)
适用场景 低竞争、频繁读取 高竞争、频繁写入

3.3 核心逻辑:一张图看懂

我画了张图,帮你理解LongAdder的设计思想:

LongAdder 核心结构图 Thread-1 Thread-2 Thread-3 Thread-N Cell[0] Cell[1] Cell[2] Cell[N] sum() 求和 返回最终结果 每个线程映射到不同的Cell,互不竞争 读取时遍历所有Cell求和,保证最终一致性

3.4 源码级别的对比

咱们看看LongAdderadd()方法核心逻辑:

public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    // 1. 先尝试更新base(类似AtomicInteger)
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        // 2. 如果base竞争失败,进入Cell数组
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[getProbe() & m]) == null ||
            !(uncontended = a.cas(v = a.value, v + x)))
            // 3. 如果Cell也竞争失败,扩容或重试
            longAccumulate(x, null, uncontended);
    }
}

我解释一下这段代码:

  • 第一步:先尝试更新base。如果没竞争,直接成功——跟AtomicInteger一样快。
  • 第二步:如果base失败了,说明有竞争。那就找自己的Cell去更新。
  • 第三步:如果Cell也冲突了(比如哈希碰撞),就调用longAccumulate去扩容或者重新哈希。

关键点:LongAdder不是完全替代AtomicInteger。它用「空间换时间」,内部维护了一个Cell数组。每个Cell本质上就是一个AtomicLong。所以内存占用会大一些。

3.5 实战选择指南

我建议你这样选:

  1. 低竞争场景(线程数 < 4):用AtomicInteger。简单、轻量、读取快。
  2. 高竞争场景(线程数 > 8):用LongAdder。写入性能线性提升。
  3. 需要精确计数的场景:比如交易系统的订单号生成,用AtomicLong。因为LongAdder的sum()不是强一致的。
  4. 统计类场景:比如QPS计数、延迟分布统计,无脑上LongAdder。

我个人的经验:在交易系统的行情处理模块里,我用LongAdder统计每秒收到的行情笔数。100万笔/秒的行情下,AtomicLong的CPU开销占5%,换成LongAdder后降到0.5%。这个优化太值了。

3.6 避坑指南

我曾经犯过一个错误:在需要强一致性的场景用了LongAdder。

比如交易系统的资金余额更新。我用LongAdder来累加,结果sum()的时候,其他线程还在写。读到的值总是差一点。嗯,这个坑踩得挺疼。

记住:LongAdder的sum()不是原子操作。它遍历Cell数组时,其他线程可能正在修改。所以它只保证最终一致性,不保证强一致性。

另外,LongAdderreset()方法也要小心。它重置所有Cell,但如果你在重置的同时有其他线程在写,数据就丢了。我一般用sumThenReset(),它先求和再重置,相对安全。

好了,从AtomicIntegerLongAdder,本质就是「从单点竞争到分片竞争」的思路。这个思想在交易系统里无处不在——分库分表、分区队列、分片缓存,都是这个道理。

专注资料整理