一、为什么会发生指令重排序?
CPU、编译器不会严格按照你写代码的顺序一行一行执行:
- 有些指令阻塞等待(读写内存、IO),CPU 闲着;
- 编译器 / CPU 调整指令顺序,把能并行执行的指令提前跑,充分利用硬件资源,提升吞吐。
前提:单线程下,重排不能改变最终执行结果,这是重排序遵守的基础规则(as-if-serial 语义)。
举个简单例子:
int a = 1; int b = 2; int c = a + b;编译器可以重排成:
int b = 2; int a = 1; int c = a + b;单线程结果完全一样,无任何影响,所以允许重排。
二、三种重排序来源
1. 编译器优化重排序(JIT 编译器)
JIT 在编译字节码为机器码时,调整语句执行顺序,消除冗余、优化分支、合并读写。
2. CPU 指令重排(处理器乱序执行)
CPU 有多级流水线、多个运算单元。 一条指令读内存很慢,CPU 不会干等,会先执行后面不依赖它数据的指令,也就是乱序执行。
3. 内存系统重排序(缓存、写缓冲区)
每个 CPU 核心有独立高速缓存、写缓冲,写操作不会立刻刷入主存,写入缓冲延时刷新,宏观上看起来读写顺序颠倒。
三、核心规则:as-if-serial 语义
不管怎么重排序,单线程程序的执行结果不能改变。 有数据依赖的两行代码,绝对不能重排:
- 写后读:
a=1; b=a;不能颠倒; - 读后写:
b=a; a=2;不能颠倒; - 写后写:
a=1; a=2;不能颠倒。
无数据依赖的代码,才允许随意调换顺序。
四、重排序带来的多线程安全问题(重点考点)
as-if-serial 只保证单线程正确,多线程之间没有这个约束,会出现逻辑错乱,最经典例子:DCL 双重检查锁单例。
instance = new Singleton();这一行代码底层拆成三步:
- 分配一块内存空间;
- 在内存中初始化 Singleton 对象(构造函数赋值);
- 将 instance 引用指向这块内存地址。
这三步不存在数据依赖,编译器 / CPU 可以重排顺序为:1 → 3 → 2。
多线程故障场景:
- 线程 A 执行 instance=new Singleton (),重排执行 1 分配内存、3 赋值引用,还没执行初始化 2;
- 此时线程 B 进入判断
if(instance == null),发现 instance 不为空; - 线程 B 直接使用 instance,拿到一个未初始化完成的半空对象,出现空指针 / 数据错乱。
这就是指令重排序导致的并发 bug,解决方案:用volatile修饰 instance,插入内存屏障禁止这三步重排。
五、volatile 如何禁止指令重排序:内存屏障 Memory Barrier
volatile 会在读写前后插入对应屏障,限制指令上下穿越屏障重排:
- volatile 写操作:StoreStore 屏障 + StoreLoad 屏障 所有 volatile 写前面的读写,不能重排到 volatile 写之后; volatile 写不能和后面的 volatile 读重排。
- volatile 读操作:LoadLoad 屏障 + LoadStore 屏障 volatile 读后面的读写,不能重排到 volatile 读之前。
简单记: 写 volatile:前面代码不许后移; 读 volatile:后面代码不许前移; 彻底阻断跨 volatile 变量的指令乱序。
六、补充:synchronized 也能防止重排
同步块整体具备原子性、有序性,同步代码内部的指令不会跑到锁外面,锁外代码也不会进入锁内,天然屏蔽跨同步区的重排序。
七、精简总结口述版
指令重排序是 JIT 编译器、CPU、缓存系统为优化性能打乱代码执行顺序;单线程遵循 as-if-serial 语义结果不变,但多线程无保护时会出现逻辑错乱,典型场景是 DCL 单例拿到未初始化对象;volatile 通过插入内存屏障禁止跨 volatile 变量的重排序,保证多线程下执行顺序符合代码书写逻辑。