Java Atomic 系列类:原理、应用与无锁并发编程
各位同学,大家好!今天我们来深入探讨 Java Atomic 系列类,理解其背后的原理,并学习如何利用它们进行高效的无锁并发编程。
在多线程环境下,保证共享变量的原子性操作至关重要。传统的解决方案通常是使用 synchronized
关键字或者 Lock
接口,这些方式都需要进行加锁和解锁操作,会带来上下文切换的开销,尤其是在高并发场景下,性能会受到显著影响。而 Atomic 系列类提供了一种更加轻量级的实现原子操作的方式,即无锁并发编程。
1. 原子性与可见性
首先,我们来回顾一下原子性和可见性的概念,这是理解 Atomic 类的前提。
- 原子性(Atomicity): 指一个操作是不可中断的,要么全部执行成功,要么全部不执行,不存在中间状态。
- 可见性(Visibility): 指当一个线程修改了共享变量的值,其他线程能够立即看到修改后的值。
在多线程环境下,如果不能保证原子性和可见性,就会出现各种并发问题,如数据竞争、脏读等。
2. Atomic 系列类概览
Java java.util.concurrent.atomic
包提供了多个原子类,主要分为以下几类:
- 基本类型原子类:
AtomicInteger
,AtomicLong
,AtomicBoolean
- 引用类型原子类:
AtomicReference
,AtomicStampedReference
,AtomicMarkableReference
- 数组类型原子类:
AtomicIntegerArray
,AtomicLongArray
,AtomicReferenceArray
- 字段更新器:
AtomicIntegerFieldUpdater
,AtomicLongFieldUpdater
,AtomicReferenceFieldUpdater
- 累加器和 Adder:
LongAdder
,DoubleAdder
,LongAccumulator
,DoubleAccumulator
这些类都利用了底层的 CAS (Compare and Swap) 操作来保证原子性。
3. CAS 原理
CAS 是一种乐观锁机制,它包含三个操作数:
- V: 内存地址 (要更新的变量的地址)
- A: 预期值 (线程在读取 V 时的值)
- B: 新值 (线程要更新 V 的值)
CAS 操作会比较内存地址 V 处的值是否等于 A,如果相等,则将 V 的值更新为 B,否则不做任何操作。整个比较和更新的过程是原子性的。
CAS 操作的伪代码如下:
boolean compareAndSwap(address, expectedValue, newValue) {
if (address.value == expectedValue) {
address.value = newValue;
return true;
} else {
return false;
}
}
CAS 的优点:
- 避免了加锁和解锁带来的上下文切换开销,性能更高。
- 属于乐观锁,适用于读多写少的场景。
CAS 的缺点:
- ABA 问题: 如果一个变量的值先是从 A 变为 B,然后再变回 A,CAS 操作会认为该变量没有发生变化,但实际上可能已经被其他线程修改过。
- 自旋开销: 如果 CAS 操作一直失败,线程会不断地重试,造成 CPU 资源浪费。
4. AtomicInteger 源码解析
我们以 AtomicInteger
为例,来分析其内部实现原理。
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// 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;
public AtomicInteger(int initialValue) {
value = initialValue;
}
public final int get() {
return value;
}
public final void set(int newValue) {
value = newValue;
}
public final int getAndSet(int newValue) {
while (true) {
int current = get();
if (compareAndSet(current, newValue))
return current;
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final int getAndIncrement() {
while (true) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
public final int incrementAndGet() {
while (true) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
// ... 其他方法省略
}
源码分析:
Unsafe
类:AtomicInteger
依赖于sun.misc.Unsafe
类,这是一个 Java 底层类,提供了直接访问内存的能力。需要注意的是,Unsafe
类通常不建议在普通应用程序中使用,因为它绕过了 JVM 的安全检查。valueOffset
:这个变量存储了value
字段在AtomicInteger
对象中的内存偏移量。通过Unsafe.objectFieldOffset()
方法可以获取到这个偏移量。volatile int value
:value
字段使用volatile
关键字修饰,保证了可见性。compareAndSet(int expect, int update)
:这是最核心的方法,它调用了Unsafe.compareAndSwapInt()
方法来实现 CAS 操作。getAndIncrement()
和incrementAndGet()
:这两个方法都使用了循环 CAS 的方式来实现原子性的自增操作。
循环 CAS:
getAndIncrement()
和 incrementAndGet()
方法都使用了 while (true)
循环,不断地尝试 CAS 操作,直到成功为止。这种方式被称为循环 CAS 或者自旋。
public final int getAndIncrement() {
while (true) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
如果 CAS 操作失败,说明有其他线程修改了 value
的值,当前线程需要重新读取 value
的值,并计算新的值,然后再次尝试 CAS 操作。
5. ABA 问题的解决方案:AtomicStampedReference
前面提到,CAS 操作存在 ABA 问题。AtomicStampedReference
类可以解决这个问题。
AtomicStampedReference
在 AtomicReference
的基础上,增加了一个版本号 (stamp)。每次修改变量时,都需要更新版本号。这样,即使变量的值从 A 变为 B,然后再变回 A,但版本号已经发生了变化,CAS 操作会检测到这种变化,从而避免 ABA 问题。
public class AtomicStampedReference<V> {
private final Pair<V> pair;
private final ReferenceFieldUpdater<AtomicStampedReference, Pair<V>> updater;
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<>(reference, stamp);
}
}
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
updater = ReferenceFieldUpdater.newUpdater(AtomicStampedReference.class, Pair.class, "pair");
}
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
updater.compareAndSet(this, current, Pair.of(newReference, newStamp)));
}
// ... 其他方法省略
}
AtomicStampedReference
的 compareAndSet()
方法需要传入四个参数:
expectedReference
:预期的引用值newReference
:新的引用值expectedStamp
:预期的版本号newStamp
:新的版本号
只有当引用值和版本号都与预期值相等时,才会更新引用值和版本号。
使用示例:
import java.util.concurrent.atomic.AtomicStampedReference;
public class AtomicStampedReferenceExample {
public static void main(String[] args) throws InterruptedException {
String initialRef = "A";
int initialStamp = 0;
AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference<>(initialRef, initialStamp);
Thread thread1 = new Thread(() -> {
String expectedReference = atomicStampedReference.getReference();
int expectedStamp = atomicStampedReference.getStamp();
// 模拟 A -> B -> A 的过程
try {
Thread.sleep(100); // 模拟线程1的操作时间
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean success = atomicStampedReference.compareAndSet(expectedReference, "B", expectedStamp, expectedStamp + 1);
System.out.println("Thread 1: A -> B, success: " + success);
expectedReference = atomicStampedReference.getReference();
expectedStamp = atomicStampedReference.getStamp();
success = atomicStampedReference.compareAndSet(expectedReference, "A", expectedStamp, expectedStamp + 1);
System.out.println("Thread 1: B -> A, success: " + success);
});
Thread thread2 = new Thread(() -> {
String expectedReference = atomicStampedReference.getReference();
int expectedStamp = atomicStampedReference.getStamp();
// 模拟线程2尝试将 A -> C
try {
Thread.sleep(200); // 确保线程1先执行完 A -> B -> A 的过程
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean success = atomicStampedReference.compareAndSet(expectedReference, "C", expectedStamp, expectedStamp + 1);
System.out.println("Thread 2: A -> C, success: " + success);
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Final Reference: " + atomicStampedReference.getReference());
System.out.println("Final Stamp: " + atomicStampedReference.getStamp());
}
}
在这个例子中,线程 1 先将 atomicStampedReference
的值从 A 变为 B,然后再变回 A,同时版本号也增加了 2。线程 2 尝试将 atomicStampedReference
的值从 A 变为 C,由于版本号已经发生了变化,CAS 操作会失败,从而避免了 ABA 问题。
6. 字段更新器 (Updater)
AtomicIntegerFieldUpdater
, AtomicLongFieldUpdater
, AtomicReferenceFieldUpdater
这三个类提供了对对象中 volatile
修饰的字段进行原子更新的能力。它们允许你对已有的类进行原子化改造,而无需修改类的源代码。
使用条件:
- 目标字段必须是
volatile
类型的。 - 目标字段不能是
private
类型的。 - 只能是实例变量,不能是类变量 (static)。
- 由于
Unsafe
类的限制,只能在当前类中访问继承下来的变量,不能访问其他类的变量。
使用示例:
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
public class AtomicIntegerFieldUpdaterExample {
static class Counter {
public volatile int count = 0;
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
AtomicIntegerFieldUpdater<Counter> updater = AtomicIntegerFieldUpdater.newUpdater(Counter.class, "count");
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
updater.incrementAndGet(counter);
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("Final Count: " + counter.count);
}
}
在这个例子中,我们使用 AtomicIntegerFieldUpdater
来原子地更新 Counter
类中的 count
字段。
字段更新器的优点:
- 无需修改类的源代码,即可实现原子更新。
- 可以对已有的类进行原子化改造。
字段更新器的缺点:
- 使用条件比较苛刻。
- 性能可能不如直接使用 Atomic 类。
7. 累加器和 Adder
LongAdder
和 DoubleAdder
是 Java 8 引入的,它们在高并发场景下,比 AtomicLong
和 AtomicDouble
具有更好的性能。
LongAdder
的原理是,它维护了一个 Cell 数组,每个 Cell 包含一个 long 类型的变量。当多个线程并发地更新 LongAdder
的值时,它们会分别更新不同的 Cell,从而减少了 CAS 操作的竞争。
适用场景:
- 高并发,写多读少的场景。
- 对最终结果的精度要求不高。
使用示例:
import java.util.concurrent.atomic.LongAdder;
public class LongAdderExample {
public static void main(String[] args) throws InterruptedException {
LongAdder adder = new LongAdder();
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
adder.increment();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("Final Sum: " + adder.sum());
}
}
在这个例子中,我们使用 LongAdder
来统计累加的总和。
8. Atomic 类选择原则
在选择 Atomic 类时,需要根据具体的场景进行权衡。
类 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
AtomicInteger/Long/Boolean |
读多写少,竞争不激烈的场景。 | 简单易用,性能较好。 | 在高并发场景下,CAS 竞争激烈,性能下降。 |
AtomicReference |
需要原子地更新引用类型的场景。 | 可以保证引用类型的原子性。 | 存在 ABA 问题。 |
AtomicStampedReference |
需要解决 ABA 问题的场景。 | 可以避免 ABA 问题。 | 实现相对复杂,性能略低于 AtomicReference 。 |
AtomicIntegerFieldUpdater |
需要对已有的类进行原子化改造,且满足使用条件的场景。 | 无需修改类的源代码,即可实现原子更新。 | 使用条件苛刻,性能可能不如直接使用 Atomic 类。 |
LongAdder/DoubleAdder |
高并发,写多读少的场景,对最终结果的精度要求不高。 | 在高并发场景下,性能优于 AtomicLong/Double 。 |
牺牲了精度,不适合对精度要求高的场景。 |
9. 无锁并发编程的注意事项
虽然 Atomic 类可以实现无锁并发编程,但仍然需要注意以下几点:
- 避免长时间的自旋: 如果 CAS 操作一直失败,线程会不断地重试,造成 CPU 资源浪费。可以考虑使用
Thread.yield()
方法来让出 CPU 资源。 - 正确处理异常: 在循环 CAS 中,需要正确处理异常,避免死循环。
- 选择合适的 Atomic 类: 根据具体的场景选择合适的 Atomic 类,避免过度使用。
- 注意内存可见性: 确保所有线程都能看到共享变量的最新值。
10. 使用 Atomic 类实现一个简单的计数器
下面我们使用 AtomicInteger
实现一个简单的计数器:
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public int increment() {
return count.incrementAndGet();
}
public int getCount() {
return count.get();
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("Final Count: " + counter.getCount());
}
}
这个计数器使用了 AtomicInteger
来保证原子性的自增操作。
11. 总结:Atomic 类简化并发编程,选择需谨慎
Atomic 系列类提供了一种高效的无锁并发编程方式,通过 CAS 操作保证了共享变量的原子性。理解 CAS 原理、ABA 问题以及各种 Atomic 类的适用场景,可以帮助我们更好地利用 Atomic 类来构建高性能的并发应用程序。在实际应用中,需要根据具体的场景选择合适的 Atomic 类,并注意无锁并发编程的注意事项。