Atomic 原子类(如 AtomicInteger)的实现原理
Atomic原子类通过 volatile 保证内存可见性,并利用 Unsafe 类的 CAS(比较并交换)乐观锁机制,以自旋方式高效地实现了无锁的原子操作。
我们来深入探讨一下 Java 中 Atomic 原子类的实现原理,并以最常用的 AtomicInteger 为例。
一句话总结其核心原理就是:通过 volatile 保证内存可见性,通过 CAS (Compare-And-Swap) 乐观锁机制保证原子性,而这两者的底层实现都依赖于 sun.misc.Unsafe 类来直接操作内存和调用 CPU 指令。
下面我们来分步拆解这个原理。
1. 问题背景:为什么需要原子类?
在多线程环境下,对一个共享变量进行 i++ 这样的操作不是原子性的。它至少包含三个步骤:
- 读取
i的当前值。 - 将读取到的值 加 1。
- 将计算后的结果 写回
i。
在并发场景下,多个线程可能同时执行到第一步,读取到相同的值,然后各自加一再写回,导致最终结果小于预期,这就是线程安全问题。
通常的解决方案是使用 synchronized 关键字或 Lock:
public synchronized void increment() {
count++;
}
这种方式被称为悲观锁,它假设总会有冲突发生,所以每次操作前都先加锁,阻塞其他线程。虽然保证了安全,但在竞争不激烈的情况下,加锁和解锁的开销(涉及用户态和内核态的切换)会造成性能瓶न्दा。
原子类提供了一种无锁 (Lock-Free) 的、性能更高的替代方案。
2. 核心组件一:volatile 关键字 - 保证可见性
我们打开 AtomicInteger 的源码,首先会看到它的核心成员变量:
public class AtomicInteger extends Number implements java.io.Serializable {
// ...
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset; // 内存偏移地址
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value; // 核心!
// ...
}
这里的 value 变量被 volatile 修饰。volatile 关键字主要有两个作用:
保证可见性 (Visibility):当一个线程修改了
value的值,这个修改会立刻被刷新到主内存中。同时,其他线程在读取value之前,会先使自己的本地缓存失效,强制从主内存中重新读取最新值。这就确保了所有线程看到的value始终是一致的。禁止指令重排序 (Happens-Before):
volatile会插入内存屏障,防止编译器和处理器为了优化而随意改变代码的执行顺序,确保代码的逻辑正确性。
可见性是 CAS 操作成功的基础。 如果没有 volatile,一个线程可能基于一个过期的值去执行 CAS 操作,导致逻辑错误。
3. 核心组件二:CAS (Compare-And-Swap) - 保证原子性
CAS 是原子类实现原子操作的基石。它是一种乐观锁机制。
CAS 是什么?
CAS 是一种硬件级别的原子指令,它接受三个参数:
- V (Variable):要更新的内存地址(或变量)。
- A (Assumed/Expected):期望的旧值。
- B (New Value):要更新的新值。
CAS 操作的逻辑是:“我认为内存地址 V 的值应该是 A,如果是,那就把它更新为 B;如果不是,说明在我准备更新的期间有其他线程已经修改了它,那我就什么都不做,并返回失败。”
这个 "比较并交换" 的过程是一条 CPU 原子指令(例如 x86 架构下的 CMPXCHG 指令),由硬件保证其不可中断,从而实现了原子性。
CAS 如何在 AtomicInteger 中工作?
我们以 AtomicInteger.getAndIncrement() 方法(等同于 i++)为例,看看它的简化版源码:
public final int getAndIncrement() {
// this 是当前 AtomicInteger 对象
// valueOffset 是 value 字段的内存偏移地址
// 1 是要增加的值
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// Unsafe.java 中的 getAndAddInt 方法
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
// 1. 读取当前 volatile 变量的值
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta)); // 2. CAS 尝试更新
return v; // 3. 返回更新前的值
}
这个 do-while 循环就是 CAS 的经典使用模式:
- 读取 (Read):通过
getIntVolatile读取value的当前值(假设为v)。 - 计算 (Modify):在本地计算出新值
v + delta(这里delta是 1)。 - 交换 (Compare-And-Swap):调用
compareAndSwapInt方法,尝试将value从v更新为v + 1。- 如果成功:说明从读取
v到现在,没有其他线程修改过value。更新完成,循环结束。 - 如果失败:说明在步骤 1 和 3 之间,有其他线程抢先修改了
value。此时compareAndSwapInt返回false,循环继续。 - 重试:循环回到步骤 1,重新读取最新的
value值,然后再次尝试,直到成功为止。
- 如果成功:说明从读取
这个不断重试的过程,也叫自旋 (Spinning)。由于大部分情况下线程竞争不激烈,CAS 操作一次就能成功,所以性能远高于需要操作系统介入的悲观锁。
4. 幕后推手:sun.misc.Unsafe 类
Java 本身无法直接调用 CPU 的 CAS 指令。它通过一个名为 sun.misc.Unsafe 的特殊类来实现。这个类提供了类似 C++ 指针的、直接操作内存的能力。
AtomicInteger 在静态代码块中,通过反射获取 Unsafe 实例,并计算出 value 字段在对象内存布局中的偏移地址 (offset)。
之后,所有对 value 的操作,如 compareAndSwapInt,都是通过 Unsafe 的方法,传入对象实例、字段偏移地址和要操作的值来完成的。Unsafe 类的方法在底层会调用 JVM 的内部方法,最终映射到 CPU 的原子指令上。
5. CAS 的 ABA 问题
CAS 机制并非完美,它存在一个经典的 "ABA" 问题。
- 问题描述:
- 线程 T1 读取内存值 V 为 A。
- 线程 T2 介入,将 V 的值从 A 改为 B,然后又改回 A。
- 线程 T1 执行 CAS 操作,发现内存值 V 仍然是 A,于是成功更新。
对于 T1 来说,它没有意识到值虽然没变,但状态其实已经发生过变化。在某些业务场景下(例如链表操作),这可能会导致严重问题。
- 解决方案:
Java 提供了AtomicStampedReference类来解决 ABA 问题。它的原理是为每个值增加一个版本号(或叫“戳”,Stamp)。CAS 操作时不仅要比较值,还要比较版本号。当值被修改时,版本号也随之改变。这样,即使值从 A -> B -> A,版本号也已经从 1 -> 2 -> 3,T1 的 CAS 操作会因为版本号不匹配而失败。
6. 高并发下的性能瓶颈与 LongAdder
在高并发场景下,如果大量线程同时更新同一个 AtomicInteger,会导致 CAS 操作的失败率急剧上升。大量线程会陷入自旋等待,不断重试,消耗大量 CPU 资源,性能反而会下降。
为了解决这个问题,Java 8 引入了 LongAdder。
LongAdder的原理:分段锁/数据分离。- 它内部维护一个
base变量和一个Cell[]数组。 - 在没有竞争或竞争很低时,直接通过 CAS 更新
base值,和AtomicLong类似。 - 当更新
base失败(出现竞争)时,线程会尝试去更新Cell数组中某个槽(Cell)的值。每个线程通过哈希等算法映射到自己的Cell上,从而将竞争分散到多个Cell中。 - 获取总和时(调用
sum()方法),再将base和所有Cell的值累加起来。
- 它内部维护一个
LongAdder 用空间换时间的思想,极大地减少了热点更新的竞争,使得其在统计、计数等高并发场景下的吞吐量远超 AtomicLong。但它不适合需要强一致性的、依赖于 CAS 返回值的场景。
总结
| 特性 | 实现方式 | 作用 |
|---|---|---|
| 可见性 | private volatile int value; |
确保多线程间对 value 值的修改能被立即看到。 |
| 原子性 | Unsafe.compareAndSwapInt() |
利用 CPU 的原子指令,实现无锁的、乐观的原子更新。 |
| 机制 | 自旋循环 (Spinning Loop) | 当 CAS 失败时,不断重试,直到成功。 |
| 底层桥梁 | sun.misc.Unsafe |
连接 Java 代码和底层硬件指令,提供直接内存操作能力。 |
Atomic 原子类的实现是现代并发编程中一个非常经典的例子,它巧妙地利用了硬件特性,避免了传统锁的性能开销,为我们提供了一个高效、轻量级的并发工具。