1. 前言:为什么需要原子类?

在多线程环境下,我们经常需要对一个变量执行 读-改-写 操作,比如 count++。这个简单的操作在 Java 中实际上是三步:读取当前值 → 加1 → 写回内存。如果多个线程同时执行,就会产生线程安全问题。

传统解决方案:加锁

private int count = 0;
public synchronized void increment() {
    count++;
}

synchronized 能保证原子性,但代价是线程阻塞与上下文切换,在高并发、低竞争的场景下显得“太重”。

另一种选择:volatile?
volatile 能保证可见性,禁止指令重排序,但 不能保证复合操作的原子性count++ 这类操作依然不安全。

于是,基于 CAS(Compare And Swap)无锁算法 的原子类应运而生,它们可以在不加锁的情况下安全地更新变量,性能远超 synchronized。今天,我们就来彻底吃透 java.util.concurrent.atomic 包下的所有原子类。


2. 原子类全家福:五大分类一览

JUC 原子类分为以下五大阵营:

分类核心类用途
基本类型AtomicInteger, AtomicLong, AtomicBoolean原子更新 int、long、boolean
数组类型AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray原子更新数组的某个索引元素
引用类型AtomicReference, AtomicStampedReference, AtomicMarkableReference原子更新对象引用,后两者解决 ABA 问题
属性更新器AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater原子更新类中 volatile 字段
高性能累加器LongAdder, DoubleAdder, LongAccumulator, DoubleAccumulator高并发下更优的累加性能

它们虽然外表不同,但灵魂都是 CAS + volatile,让我们先看清底层原理。


3. CAS 与 volatile:原子类的底层双引擎

3.1 CAS 是什么?

CAS 全称 Compare And Swap,是一条 CPU 原子指令。它需要三个操作数:

  • 内存位置 V
  • 预期原值 A
  • 新值 B

执行逻辑: 如果内存位置 V 的值与预期原值 A 相等,就将其更新为新值 B,否则不做任何操作。整个比较与交换是一个硬件原子操作,不可分割。

Java 通过 sun.misc.Unsafe 类提供的 compareAndSwapIntcompareAndSwapLong 等 native 方法使用 CAS。

3.2 CAS 的自旋循环

原子类几乎每个方法都基于“CAS 自旋”实现,伪代码如下:

do {
    当前值 = 获取最新值;
    新值 = 计算新值(当前值);
} while (!compareAndSet(当前值, 新值));

线程不会挂起,失败就重试,直到成功为止,这就是“无锁”的秘密。

3.3 volatile 的作用

原子类内部的值字段都是 volatile 的,如:

private volatile int value;

保证线程间可见性,使得 get() 方法总能获取最新值,同时禁止重排序,配合 CAS 才能正确工作。

⚠️ 注意:CAS 存在经典的 ABA 问题,后面会专门讲解并给出解决方案。


4. 基本类型原子类:AtomicInteger / AtomicLong / AtomicBoolean

这三个最常用,以 AtomicInteger 为例(AtomicLong 同理,AtomicBoolean 是它的特殊形态)。

4.1 核心方法

AtomicInteger ai = new AtomicInteger(0);

int get();                    // 获取当前值
void set(int newValue);       // 直接赋值
int getAndSet(int newValue);  // 设置新值并返回旧值
int incrementAndGet();        // ++i
int getAndIncrement();        // i++
int decrementAndGet();        // --i
int getAndDecrement();        // i--
int addAndGet(int delta);     // 加 delta 并返回新值
int getAndAdd(int delta);     // 加 delta 并返回旧值
boolean compareAndSet(int expect, int update); // CAS 核心

4.2 实战:线程安全计数器

public class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();
    }

    public int get() {
        return count.get();
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicCounter counter = new AtomicCounter();
        int threadCount = 10;
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    counter.increment();
                }
                latch.countDown();
            }).start();
        }

        latch.await();
        System.out.println(counter.get()); // 稳定输出 100000
    }
}

不用锁,代码简洁,线程安全。

4.3 AtomicBoolean 的小技巧

AtomicBooleancompareAndSet 可用于实现 一次性开关,比如保证某段逻辑只执行一次:

AtomicBoolean initialized = new AtomicBoolean(false);
if (initialized.compareAndSet(false, true)) {
    // 执行初始化,保证只有一个线程能进入
}

5. 数组原子类:原子操作数组的每一个元素

当需要对数组内元素进行原子更新时,AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray 就派上用场了。

5.1 示例:AtomicIntegerArray

int[] arr = {1, 2, 3, 4, 5};
AtomicIntegerArray atomicArr = new AtomicIntegerArray(arr);

// 对索引 0 进行原子加 10
atomicArr.addAndGet(0, 10);
System.out.println(atomicArr.get(0)); // 11
// 注意:原始数组 arr 不会改变,原子类拷贝了一份
System.out.println(arr[0]);           // 1

5.2 注意事项

  • 构造时传入的数组会被拷贝一份,原子类操作的是新数组,原数组不变。
  • 数组索引从 0 开始,越界会抛出异常。
  • 适用于需要对数组元素并发修改的场景,比如多线程统计不同维度的指标。

6. 引用类型原子类:AtomicReference 与 ABA 问题终结者

6.1 AtomicReference

当需要原子更新的数据是对象引用时使用。

class User {
    String name;
    int age;
    // 构造器、getter/setter...
}

AtomicReference<User> ref = new AtomicReference<>(new User("张三", 20));
ref.updateAndGet(user -> {
    user.setAge(21);
    return user;
});

6.2 ABA 问题的根源与危害

CAS 只比较,不关心修改历史。如果一个变量被从 A 改成 B,又改回 A,CAS 会认为它没变,但实际上可能已经发生了逻辑错误。

经典案例:单向链表栈的操作
假设栈顶为 A → B → C,线程1 想用 CAS 将栈顶从 A 改为 B,但在 CAS 前:

  • 线程2 将栈顶 A 弹出,弹出 B,又压入 A(此时栈变成 A → C)。
  • 线程1 执行 CAS 发现栈顶仍为 A,于是将栈顶改为 B,结果 B 本已不在栈中,导致栈结构破坏。

6.3 解决方案:AtomicStampedReference

它通过附加一个 版本号(stamp) 来解决 ABA 问题,CAS 时同时比较引用和版本号。

// 初始化引用为 "A",版本号为 0
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

int[] stampHolder = new int[1];
String oldRef = ref.get(stampHolder);
int oldStamp = stampHolder[0];

// 尝试将 "A" 改为 "B",版本号 +1
boolean success = ref.compareAndSet(oldRef, "B", oldStamp, oldStamp + 1);

每一次修改都强制更新 stamp,从而避免了“值相同历史不同”的迷惑。

6.4 AtomicMarkableReference

AtomicStampedReference 类似,但只维护一个 布尔标记,适用于关心“是否被动过”而不是“改了多少次”的场景(如垃圾回收标记)。


7. 属性更新器:让普通对象的字段也“原子化”

如果你的类中有一个 volatile 字段,并且希望在不修改类结构(不改为原子类)的情况下对其进行原子更新,属性更新器是完美的选择。

7.1 AtomicIntegerFieldUpdater 示例

public class GameScore {
    // 必须 volatile
    volatile int score;

    public GameScore(int score) { this.score = score; }
}

// 创建更新器,第一个参数是包含字段的类,第二个是字段名
AtomicIntegerFieldUpdater<GameScore> updater =
    AtomicIntegerFieldUpdater.newUpdater(GameScore.class, "score");

GameScore gs = new GameScore(100);
updater.incrementAndGet(gs); // 101

7.2 使用限制(踩坑点)

  • 字段必须是 volatile 且非 private(或通过反射可访问)。
  • 不能是 static 字段(AtomicIntegerFieldUpdater 不支持 static,有 AtomicLongFieldUpdater 等相同限制;static 字段可直接用原子类包装)。
  • 类型匹配:例如 AtomicIntegerFieldUpdater 只能更新 int 字段。

优点:节省内存,当有大量对象实例时,不用每个字段都包装成原子对象,用一个静态的 Updater 就能批量操作。


8. 高性能累加器:JDK8 的 LongAdder 为何吊打 AtomicLong?

AtomicLong 在高并发下,大量线程 CAS 自旋竞争同一个变量,效率会急剧下降。JDK8 引入了 LongAdder,通过 分段累加 思想解决热点竞争问题。

8.1 分段累加原理

LongAdder 内部维护了一个 base 值和一组 Cell 数组:

  • 线程调用 add() 时,先尝试对 base CAS,成功则返回。
  • 失败则哈希到某个 Cell,对其内的 value 进行 CAS 累加。
  • 最终求和时会累加 base 和所有 Cell 的值。

这样就把单一热点分散到多个 Cell,并发越高优势越明显。

8.2 实战对比

LongAdder adder = new LongAdder();
adder.increment();
adder.add(10);
long sum = adder.sum(); // 获取总和(不是原子快照)

8.3 LongAccumulator:更灵活的自定义累加

LongAdder 只支持加/减,LongAccumulator 允许自定义二元运算。

// 计算最大值
LongAccumulator maxAcc = new LongAccumulator(Long::max, Long.MIN_VALUE);
maxAcc.accumulate(5);
maxAcc.accumulate(10);
System.out.println(maxAcc.get()); // 10

你可以实现求积、最小值等任意逻辑,内部原理同样是分段累加(使用 Cell 数组)。

DoubleAdder / DoubleAccumulator 是 double 版本,原理一致。


9. 性能测试与选型建议

9.1 粗略基准测试 (JMH 思路)

网上大量测试表明:

  • 低并发(1~2线程):AtomicLong 略优于 LongAdder(额外 Cell 维护开销)。
  • 高并发(64线程以上):LongAdder 吞吐量是 AtomicLong几倍甚至十几倍

9.2 选型指南

场景推荐工具理由
简单计数器,低竞争AtomicInteger/Long实现简单,内存占用小
高并发统计(如 QPS 计数器)LongAdder高吞吐,牺牲少许精确性
需要自定义累加逻辑LongAccumulator灵活性高
存在 ABA 风险的对象引用AtomicStampedReference带版本号,安全
类字段需要原子更新且对象大量创建属性更新器节省内存
需要原子操作数组索引数组原子类减少锁粒度

💡 注意:LongAddersum() 方法在并发修改时可能不是精确快照,如果需要严格的原子统计(如余额),请用 AtomicLong 或加锁。


10. 总结

原子类是 JUC 无锁编程的基石,它们用 CPU 级别的 CAS 指令替换了重量级锁,大幅提升了轻量级竞争场景的性能。从基础的 AtomicInteger 到解决 ABA 的 AtomicStampedReference,再到 JDK8 的高性能 LongAdder,每一种都有其特定的舞台。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐