指令重排序
代码写在前面的,真的会先执行吗?
在并发编程里,答案是不一定。编译器、CPU、缓存都可能「悄悄」调整执行顺序,这就是指令重排序。
三种重排序类型
| 类型 | 发生在哪里 | 影响 |
|---|---|---|
| 编译器重排序 | JVM/JIT 编译器 | 改变代码逻辑(单线程内无害) |
| 指令级重排序 | CPU 流水线 | 改变指令执行顺序 |
| 内存重排序 | CPU 缓存 | 改变内存可见顺序 |
as-if-serial 语义
在不改变单线程程序执行结果的前提下,编译器和处理器可以任意重排序。
java
// 单线程下,无论如何重排序,c 都是 3
int a = 1;
int b = 2;
int c = a + b;问题在于:多线程环境下,这种优化可能暴露 bug。
重排序导致的问题
java
public class ReorderingProblemDemo {
private static int a = 0, b = 0, x = 0, y = 0;
public static void main(String[] args) throws InterruptedException {
int count = 0;
for (int i = 0; i < 10000; i++) {
a = 0; b = 0; x = 0; y = 0;
count++;
Thread t1 = new Thread(() -> {
a = 1;
x = b;
});
Thread t2 = new Thread(() -> {
b = 1;
y = a;
});
t1.start();
t2.start();
t1.join();
t2.join();
// 理论分析:
// 如果没有重排序,不可能出现 (x=0, y=0)
// 因为至少有一个线程先执行
// 但如果发生了内存重排序,可能出现 (x=0, y=0)
if (x == 0 && y == 0) {
System.out.println("检测到重排序导致的问题!x=0, y=0,循环次数: " + count);
break;
}
}
if (count >= 10000) {
System.out.println("未检测到问题(但重排序风险仍然存在)");
}
}
}这个实验可能检测到 (x=0, y=0) 的组合,因为 CPU 的内存重排序。
happens-before 规则:多线程中的「秩序」
JMM 定义了 happens-before 规则,在满足规则的情况下,禁止重排序:
┌──────────────────────────────────────────────────┐
│ happens-before 禁止的重排序 │
├──────────────────────────────────────────────────┤
│ │
│ 1. volatile 写 HB volatile 读 │
│ ┌────────┐ ┌────────┐ │
│ │ volatile │ ──────► │ volatile │ │
│ │ 写 │ HB │ 读 │ │
│ └────────┘ └────────┘ │
│ 写操作不能重排序到读之后 │
│ │
│ 2. synchronized:unlock HB lock │
│ ┌────────┐ ┌────────┐ │
│ │ unlock │ ──────► │ lock │ │
│ └────────┘ └────────┘ │
│ 前一个线程的写,对后一个线程可见 │
│ │
│ 3. 程序顺序规则:单线程内前 HB 后 │
│ a = 1; │
│ b = 2; │
│ // a = 1 不能重排序到 b = 2 之后 │
│ │
└──────────────────────────────────────────────────┘锁与顺序性
synchronized 块内的操作不会被重排序到块外:
java
public class LockOrderingDemo {
private static int a = 0;
private static int b = 0;
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (lock) {
a = 1;
b = 2;
// synchronized 内部的指令不能重排序到外部
// 进入时刷新缓存,退出时写回主内存
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
int x = a;
int y = b;
System.out.println("x=" + x + ", y=" + y);
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}volatile 防止重排序
java
public class VolatileReorderDemo {
// 普通变量:可能重排序
private int number = 0;
private boolean ready = false;
// volatile 变量:禁止重排序
private volatile int volNumber = 0;
private volatile boolean volReady = false;
}volatile 的内存屏障:
volatile 写之前: StoreStore 屏障(禁止之前的写被重排序到屏障后)
volatile 写: StoreLoad 屏障(强制写刷新到主内存)
volatile 读之后: LoadLoad 屏障(禁止之后的读被重排序到屏障前)
LoadStore 屏障(禁止之后的写被重排序到屏障前)单例模式与重排序
这是最经典的重排序问题:
java
public class SingletonReorder {
// ❌ 危险示例:可能返回未完全初始化的对象
private static SingletonReorder instance;
public static SingletonReorder getInstanceWrong() {
if (instance == null) {
synchronized (SingletonReorder.class) {
if (instance == null) {
instance = new SingletonReorder();
// new SingletonReorder() 实际执行:
// 1. 分配内存
// 2. 调用构造函数
// 3. 将引用赋值给 instance
// 如果 2 和 3 重排,其他线程可能看到未初始化的对象
}
}
}
return instance;
}
// ✅ 正确示例:volatile 防止重排序
private static volatile SingletonReorder instance2;
public static SingletonReorder getInstanceRight() {
if (instance2 == null) {
synchronized (SingletonReorder.class) {
if (instance2 == null) {
instance2 = new SingletonReorder();
// volatile 防止重排序:
// 1. 分配内存
// 2. 调用构造函数
// 3. 将引用赋值给 instance
// 顺序固定,结果一致
}
}
}
return instance2;
}
private SingletonReorder() {}
}数据依赖性
如果两个操作之间有数据依赖关系,编译器不能重排序:
写后读: x = 1; y = x; ← 不能重排序(y 依赖 x)
写后写: x = 1; x = 2; ← 不能重排序(后一个写覆盖前一个)
读后写: y = x; x = 2; ← 不能重排序(x 的写依赖 x 的读)但跨线程的数据依赖,编译器无法感知——这就是并发 bug 的根源。
重排序对比表
| 机制 | 防止自身重排序 | 防止对其他操作的重排序 |
|---|---|---|
| volatile | ✅ 自身 | ✅ 前后操作 |
| synchronized | ✅ | ✅ 块内所有操作 |
| final | ✅ 安全发布后 | ✅ 安全发布后 |
| 程序顺序 | ✅ 单线程内 | - |
注意事项
- 单线程内重排序无害:as-if-serial 保证正确性
- 跨线程需要 happens-before:否则可能出现奇怪的结果
- volatile 防止重排序:不仅保证可见性,也保证有序性
- synchronized 防止重排序:进入刷新缓存,退出写回缓存
- 数据依赖性只在单线程内有效:多线程间无意义
要点回顾
- 重排序有三种:编译器、指令级、内存重排序
- as-if-serial 只对单线程有效
- happens-before 规则限制了多线程间的重排序
volatile通过内存屏障防止重排序synchronized块内的操作不会被重排序到块外- 单例模式中
volatile防止重排序导致访问未初始化对象
相关链接
- Happens-Before - 重排序的约束规则
- 可见性 - 重排序对可见性的影响
- volatile - 防止重排序的具体机制
- 并发问题排查 - 重排序导致的问题排查
