指令重排重排的是无数据依赖的指令顺序,是jvm和cpu为提升单线程性能做的合法优化;volatile通过内存屏障禁止其读写附近的重排;synchronized和lock借助happens-before和内存屏障保证临界区有序。

指令重排到底在重排什么?
指令重排不是Java“乱写代码”,而是JVM编译器(包括JIT)和CPU硬件为提升单线程性能,对**无数据依赖的指令**做的合法优化。比如:
int a = 1; // 语句1 int b = 2; // 语句2 int c = a + b; // 语句3
语句1和2谁先执行,不影响结果,所以可能被调换顺序;但语句3一定在1、2之后——因为有数据依赖。问题在于:这种“对单线程透明”的重排,在多线程下会打破你脑中的执行假设。
volatile为什么能禁止重排?
volatile不是魔法,它通过插入**内存屏障(Memory Barrier)** 强制约束指令顺序。具体来说:
- 写
volatile变量前,插入StoreStore屏障 → 禁止该写操作与前面的普通写重排 - 写
volatile变量后,插入StoreLoad屏障 → 禁止该写与后续的读操作重排 - 读
volatile变量后,插入LoadLoad屏障 → 禁止该读与后面的普通读重排
注意:volatile只禁止**它自身读写附近的重排**,不保证其他非volatile变量之间的顺序。例如下面这段代码依然危险:
立即学习“Java免费学习笔记(深入)”;
private volatile boolean inited = false; private String config; <p>// 线程A<br /> config = "loaded"; // 普通写,可能被重排到inited=true之后<br /> inited = true; // volatile写,但拦不住上面那行
正确做法是把config也声明为volatile,或改用synchronized/Lock保证临界区整体有序。
synchronized 和 Lock 怎么顺便解决重排?
synchronized和ReentrantLock的加锁/解锁操作,天然构成JMM的Happens-Before关系。这意味着:
- 解锁前的所有内存操作,对后续获取同一把锁的线程可见
- 解锁操作本身带有StoreLoad屏障,强制刷新缓存到主内存
- 加锁操作带有LoadLoad+LoadStore屏障,清空本地缓存并重新加载
所以,哪怕你没用volatile,只要共享变量的读写都包裹在同一个锁保护的临界区内,重排就构不成威胁。但要注意:锁对象必须一致,且所有访问路径都要走同一把锁——漏掉一个get()方法没加锁,整个防线就垮了。
哪些场景最容易栽在重排上?
真实项目中最常翻车的不是计数器,而是“初始化+标志位”这类双重检查模式。典型错误:
private static Singleton instance; private static boolean inited = false; <p>// 线程A<br /> instance = new Singleton(); // 分三步:分配内存→调构造→赋值给instance<br /> inited = true; // 可能被重排到构造完成前!
线程B看到inited == true就直接用instance,但此时对象可能还没构造完,触发NPE或诡异状态。这就是著名的“DCL失效”问题。解决方案只有两个:
- 把
instance声明为volatile(JDK5+起有效,靠其禁止重排+保证可见性) - 彻底放弃懒汉式,用静态内部类或枚举——让JVM类加载机制保证初始化原子性
别信“加个Thread.sleep(1)就能等构造完”这种玄学方案,重排是编译器/CPU级行为,sleep压根不参与内存同步语义。










