跳转至

深入有序性:指令重排发生在哪些地方?

一、什么是指令重排

编译器和 CPU 为了优化性能,可能改变指令的执行顺序,但不影响单线程语义。

源码: a = 1; b = 2; c = a + b;
可能重排: b = 2; a = 1; c = a + b;  ← 单线程结果一样

二、重排发生的三个地方

1. 编译器重排

javac / JIT 编译器在不改变单线程语义的前提下调整指令顺序。

int a = 1;
int b = 2;
int c = a + b;
// 编译器可能把 b = 2 放到 a = 1 前面

2. CPU 指令级并行重排

CPU 不按程序顺序执行指令,而是按数据依赖乱序执行(Out-of-Order Execution)。

指令 1: 加载 a
指令 2: 加载 b   ← 可能先于指令 1 完成
指令 3: a + b

3. 内存系统重排

写缓冲器和失效队列导致其他核看到的顺序和实际写入顺序不一致。

即使 CPU 按顺序写,其他核看到的可能是乱序的。

三、单线程 vs 多线程

  • 单线程:重排不影响结果(as-if-serial 语义)。
  • 多线程:重排可能导致其他线程看到不一致的中间状态。

四、典型例子:单例 DCL

instance = new Singleton();

分三步: 1. 分配内存。 2. 初始化对象。 3. instance 指向内存。

JIT 重排成 1 → 3 → 2:

线程 A: 分配内存 → instance 指向内存(未初始化)
线程 B: 看到 instance != null,直接用 → 拿到未初始化对象

volatile 禁止这种重排。

五、happens-before

JMM 用 happens-before 规则定义什么不能重排:

  • 程序顺序规则:单线程内前一个操作 happens-before 后一个。
  • volatile 规则:volatile 写 happens-before 后续 volatile 读。
  • 锁规则:unlock happens-before 后续 lock。
  • 传递性。

关键点

重排在单线程下是透明的,多线程下是 bug 根源。volatile 和 synchronized 就是告诉编译器/CPU"这里不能乱"。