指令重新排序是编译器或处理器在不改变单线程序语义的情况下调整指令实际执行顺序以提高执行效率的行为。它本身不是错误,而是现代软硬件协调优化的必然结果;但一旦进入多线程环境,缺乏同步机制,可能会导致变量读取混乱、逻辑故障等隐藏问题。
指令重排序的三种来源
重排序不是凭空发生的,主要来自三个层次:
- 编译器优化重排:当Java源码编译成字节码时,Javac或JIT可能会交换不依赖的相邻句子。例如
a = 1; flag = true;可以重新安排flag = true; a = 1;,单线程下的结果保持不变,但可以先看到其他线程flag == true却读到a == 0。 - 处理器指令级重排:CPU使用乱序执行(Out-of-Order Execution)提高装配线的利用率。只要两个指令不共享寄存器或内存地址,就可以更改执行顺序,如延迟写作和提前阅读操作。
- 内存系统重排:CPU缓存,写缓冲区(Store Buffer)和无效队列(Invalidate Queue)随着线程的存在,一个线程的写入延迟了另一个线程的可见性,“看起来”就像读写顺序被颠倒了——这是“伪重排序”,但效果相当于真正的重排。
必须遵守重排序的底线:as-if-serial语义
所有重排序都必须满足一个前提:从单线程的角度来看,程序行为与按代码顺序执行的结果完全一致。这是一种严格的约束,而不是一种选择。
关键是:数据依赖性不能被破坏。禁止在以下三种操作之间重新排列:
- 写后读(
a = 1; int x = a;) - 写后写(
a = 1; a = 2;) - 读后写(
int x = a; a = 3;)
而像int x = 1; int y = 2;这种相互无关的操作,重排完全合法,本线程无法察觉。
如何控制重排序:内存屏障和volatile语义
Java不提供直接插入硬件屏障的API,而是通过语言机制触发JVM自动插入相应的内存屏障:
- volatile变量写作操作:写入后插入StoreStore + Storeload屏障禁止与之前/之后的普通读写重新排列,并强制刷新缓存到主内存。
- volatile变量读取操作:读取前插入loadload + LoadStore屏障禁止与之前/之后的普通读写重新排列,并强制从主内存或最新缓存加载值。
- synchronized块进出:隐式包含完整的内存屏障,保证临界区内外的可见性和有序性。
- final字段结构器结束:当对象结构完成时,将storestore屏障插入final字段的写入,以确保final字段在其他线程中正确初始化(安全发布)。
典型的问题场景和验证思路
常见的问题模式是“状态标志+数据准备”分离,例如:
危险写法:
data = 42; // 准备数据<br> ready = true; // 发布就绪
可以看到另一个线程ready == true但data == 0,由于这两个句子被重新排列或缓存不同步。
修复方式: – 把ready声明为volatile; – 或用synchronized包裹两行; – 或使用AtomicBoolean+ 内存屏障语义。
可通过循环压力测试(如反复启停两个线程读写共享变量)复现(0,0)类异常结果是重排序存在的直接证据。
基本上就是这样。理解重排序不依赖死记规则,而是掌握“单线程序保护、多线程显式同步”的核心逻辑。