编译器重排序是为提升单线程性能的合法优化,须遵守as-if-serial语义;其仅重排无数据依赖操作,多线程需靠volatile、synchronized等同步机制建立happens-before关系并防止控制依赖误用。

编译器重排序本身不是为了“修复错误”,而是为提升单线程性能做的合法优化,前提是必须守住 as-if-serial 语义——即重排后,单线程的执行结果与按源码顺序执行完全一致。所以“保证语义正确”的关键,不是去禁止重排,而是理解它被允许的边界,并在多线程场景中用同步机制补足缺失的约束。
只对无数据依赖的操作重排
编译器绝不会打乱有数据依赖的语句顺序。比如:
-
a = 1; b = a + 1;→ 不会重排,因为b依赖a的值 -
x = 5; y = 8;→ 可能重排,二者独立,交换不影响结果 -
flag = true; data = new Data();→ 若flag非volatile,JIT 可能将data初始化提前到flag = true之前(破坏多线程安全,但单线程仍符合 as-if-serial)
利用程序顺序规则建立 happens-before
同一线程内,代码书写顺序天然构成 happens-before 关系。编译器可以调整指令位置,但不能让后序操作的结果“早于”前序操作对其他线程可见。因此,正确写法是把依赖逻辑显式串起来:
- 把初始化动作放在标志位赋值之前(如
init(); ready = true;) - 配合
volatile修饰ready,就能通过 volatile 规则 + 程序顺序规则,推导出init()happens-before 其他线程读ready - 不用
volatile或synchronized,仅靠代码顺序无法跨线程传递这种保证
避免控制依赖被误用为同步手段
编译器允许重排控制依赖(比如 if (flag) doSomething(); 中的 flag 读取可能被提前),这在单线程下无害,但在多线程中极易引发问题:
立即学习“Java免费学习笔记(深入)”;
- 不要写
while (!ready) Thread.yield(); doWork();并指望ready变更立刻被看到 - 应改用
volatile boolean ready,或加锁、或用LockSupport.park/unpark等明确同步原语 - JIT 可能将循环体外提、将
ready读取缓存到寄存器,导致永远不重新加载主内存值
借助 JMM 规则引导编译器插入屏障
开发者不直接写内存屏障,但可通过语言构造“提示”JVM哪些地方不能重排:
-
volatile字段读写:触发 JVM 插入LoadLoad/StoreStore/StoreLoad屏障 - 进入/退出
synchronized块:分别插入获取锁和释放锁对应的屏障 - 使用
final字段构造对象:确保构造函数内对final字段的写,在对象引用发布前完成且对其他线程可见


















