0%

原子操作

原子操作:Java 并发安全的基石

在多线程环境中,对共享变量的操作若不加以控制,可能导致数据不一致(如i++这类看似原子的操作,实际包含 “读取 - 修改 - 写入” 三步)。Java 的java.util.concurrent.atomic包提供了一系列原子操作类,通过底层的 CAS(Compare-And-Swap)机制,确保操作的原子性,避免线程安全问题。

原子操作类的核心原理

原子操作类的本质是通过Unsafe 类实现的 CAS 操作,其核心思想是:乐观锁—— 假设操作不会冲突,若冲突则重试,直到成功。以最常用的AtomicInteger为例,其内部通过volatile修饰的value存储值,并借助 Unsafe 的 native 方法实现原子更新。

AtomicInteger 的核心源码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;

// 用于执行底层CAS操作的Unsafe实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
// value字段在对象中的内存偏移量(用于CAS定位)
private static final long valueOffset;

static {
try {
// 获取value字段的内存偏移量
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
}

// 存储实际值,用volatile保证可见性
private volatile int value;

// 核心方法解析:

// 1. 获取当前值(volatile保证可见性)
public final int get() {
return value;
}

// 2. 原子更新:若当前值等于预期值,则更新为新值
public final boolean compareAndSet(int expect, int update) {
// 调用Unsafe的CAS方法,原子性地比较并替换
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

// 3. 原子自增(i++的线程安全版)
public final int getAndIncrement() {
// 先获取当前值,再加1(底层通过循环CAS实现)
return unsafe.getAndAddInt(this, valueOffset, 1);
}

// 4. 原子自增(++i的线程安全版)
public final int incrementAndGet() {
// 先加1,再返回新值
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

// 其他方法:getAndDecrement()、decrementAndGet()、getAndAdd()等原理类似
}

CAS 操作的底层逻辑

unsafe.compareAndSwapInt(this, valueOffset, expect, update)的工作流程:

  1. 从内存偏移量valueOffset处读取当前值current
  2. 比较current与expect:
    • 若相等,将update写入该内存地址,返回true
    • 若不等,不做操作,返回false

整个过程由 CPU 指令原子性执行,无需加锁,效率远高于synchronized

原子操作类的常见类型与用法

java.util.concurrent.atomic包提供了多种原子类,覆盖基本类型、引用类型、数组等场景:

类型 代表类 用途
基本类型 AtomicInteger 原子更新 int 值
AtomicLong 原子更新 long 值
AtomicBoolean 原子更新 boolean 值
引用类型 AtomicReference 原子更新对象引用
AtomicStampedReference 带版本号的原子引用(解决 ABA 问题)
数组类型 AtomicIntegerArray 原子更新 int 数组元素
字段更新器 AtomicIntegerFieldUpdater 原子更新对象的 int 字段(无需字段为 volatile)

示例:使用 AtomicInteger 实现线程安全计数器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class AtomicCounter {
private final AtomicInteger count = new AtomicInteger(0);

// 线程安全的自增
public void increment() {
count.incrementAndGet();
}

// 获取当前计数
public int getCount() {
return count.get();
}

public static void main(String[] args) throws InterruptedException {
AtomicCounter counter = new AtomicCounter();
ExecutorService executor = Executors.newFixedThreadPool(10);

// 10个线程各自增1000次
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
}

executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);

System.out.println("最终计数:" + counter.getCount()); // 输出10000(无并发问题)
}
}

原子操作的局限性与解决思路

1. ABA 问题

问题:若变量值从 A 变为 B,再变回 A,CAS 会误认为值未修改,导致更新错误。
解决:使用AtomicStampedReference,为值绑定一个版本号,更新时同时检查值和版本号。

1
2
3
4
5
AtomicStampedReference<Integer> stampedRef = new AtomicStampedReference<>(100, 0);
int stamp = stampedRef.getStamp(); // 获取当前版本号

// 只有值为100且版本号为stamp时,才更新为200并递增版本号
boolean success = stampedRef.compareAndSet(100, 200, stamp, stamp + 1);

2. 高并发下的自旋开销

CAS 失败时会重试(自旋),高并发场景下可能导致大量线程空转,消耗 CPU 资源。
解决

  • 结合LongAdder(JDK 8+):高并发时分散热点,通过多个单元格累加结果,降低冲突;
  • 合理控制并发粒度,避免过度竞争。

指令重排序对原子操作的影响

JVM 和 CPU 为优化性能,会对指令进行重排序(在不影响单线程语义的前提下调整执行顺序)。但原子操作依赖操作的有序性,若重排序破坏了执行顺序,可能导致逻辑错误。

重排序的类型

  1. 编译器重排序:编译器调整代码执行顺序;
  2. 指令级重排序:CPU 将多条指令并行执行,若无数据依赖则调整顺序;
  3. 内存重排序:由于缓存和读写缓冲区,加载 / 存储操作可能看似乱序。

原子操作如何避免重排序?

  • volatile 的内存屏障:原子类中的value字段用volatile修饰,其读写会插入内存屏障(如StoreLoad),禁止重排序;
  • CAS 的原子性:CAS 操作由 CPU 指令保证原子性,不会被重排序打断。

欢迎关注我的其它发布渠道