指令重排序不破坏单线程结果但危害多线程正确性,发生在编译器、jit和cpu三层,前提为不改变单线程语义;需通过happens-before规则(如volatile、synchronized)约束顺序以保障可见性与一致性。

指令重排序本身不会破坏单线程的执行结果,但会显著影响多线程逻辑的正确性——关键在于它让“看起来有序”的代码,在真实执行时可能以不同顺序写入主内存或被其他线程观察到。
重排序发生的层级和前提
重排序发生在三个层面:编译器优化、JVM即时编译(JIT)、CPU硬件流水线。它们都遵循一个基本原则:只要不改变单线程语义(as-if-serial),就允许对无数据依赖的操作调整顺序。
- 数据依赖仅在单线程内有效:比如
a = 1; b = a;中,b = a依赖a = 1,这两句不会被重排 - 但
a = 1; flag = true;若两者无依赖关系,就可能被重排为先写flag再写a - 这种重排对单线程无害,但对另一个读
flag后再读a的线程来说,可能看到flag == true却a == 0
典型问题场景:可见性错觉与逻辑断裂
重排序常和内存可见性交织出现,造成“变量已改但另一线程看不到”或“看到部分更新”的现象。
- 一个常见例子是双重检查锁定单例:若未用
volatile修饰实例字段,构造对象的三步(分配内存 → 初始化 → 赋值给引用)可能被重排,导致其他线程拿到未初始化完成的对象引用 - 另一个例子是标志位控制循环:
ready = true和data = 42若无同步,读线程可能先看到ready == true,却读到data == 0 - 这不是 bug,而是 JMM 允许的行为——因为没有明确的 happens-before 关系约束这两个操作的顺序
靠什么约束重排序?happens-before 是核心规则
Java 不禁止重排序,而是提供 happens-before 规则来定义哪些操作必须“对其他线程可见且有序”。只要两个操作满足 happens-before,JVM 就保证前者的结果对后者可见,且不会重排其顺序。
- 程序顺序规则:同一个线程内,按代码顺序,前面的操作 happens-before 后面的
- 监视器锁规则:解锁操作 happens-before 后续对同一锁的加锁操作
- volatile 变量规则:对 volatile 字段的写 happens-before 后续对该字段的读
- 线程启动规则:主线程中
t.start()happens-before 子线程的run()开始 - 传递性:若 A happens-before B,B happens-before C,则 A happens-before C
实际编码中怎么应对
不需要手动禁止所有重排序,而是通过同步机制建立明确的 happens-before 关系。
- 共享状态变更后,用
synchronized块包裹读写,利用锁规则建立顺序 - 用
volatile修饰状态标志(如shutdown、inited),既保证可见性又禁止相关重排序 - 避免在无同步下依赖“写 A 再写 B,所以读线程一定先看到 A 再看到 B”这类隐含假设
- 构造对象后才发布引用——可借助
final字段(配合安全发布)或volatile引用
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











