硬核拆解 JUC 原子类:从 AtomicInteger 到 LongAdder,无锁编程的终极指南
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 类提供的 compareAndSwapInt、compareAndSwapLong 等 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 的小技巧
AtomicBoolean 的 compareAndSet 可用于实现 一次性开关,比如保证某段逻辑只执行一次:
AtomicBoolean initialized = new AtomicBoolean(false);
if (initialized.compareAndSet(false, true)) {
// 执行初始化,保证只有一个线程能进入
}
5. 数组原子类:原子操作数组的每一个元素
当需要对数组内元素进行原子更新时,AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray 就派上用场了。
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()时,先尝试对baseCAS,成功则返回。 - 失败则哈希到某个
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 | 带版本号,安全 |
| 类字段需要原子更新且对象大量创建 | 属性更新器 | 节省内存 |
| 需要原子操作数组索引 | 数组原子类 | 减少锁粒度 |
💡 注意:
LongAdder的sum()方法在并发修改时可能不是精确快照,如果需要严格的原子统计(如余额),请用AtomicLong或加锁。
10. 总结
原子类是 JUC 无锁编程的基石,它们用 CPU 级别的 CAS 指令替换了重量级锁,大幅提升了轻量级竞争场景的性能。从基础的 AtomicInteger 到解决 ABA 的 AtomicStampedReference,再到 JDK8 的高性能 LongAdder,每一种都有其特定的舞台。
更多推荐




所有评论(0)