指令重排序

发布时间:2026/7/15 20:28:43
指令重排序 一、为什么会发生指令重排序CPU、编译器不会严格按照你写代码的顺序一行一行执行有些指令阻塞等待读写内存、IOCPU 闲着编译器 / 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 语义不管怎么重排序单线程程序的执行结果不能改变。 有数据依赖的两行代码绝对不能重排写后读a1; ba;不能颠倒读后写ba; a2;不能颠倒写后写a1; a2;不能颠倒。无数据依赖的代码才允许随意调换顺序。四、重排序带来的多线程安全问题重点考点as-if-serial 只保证单线程正确多线程之间没有这个约束会出现逻辑错乱最经典例子DCL 双重检查锁单例。instance new Singleton();这一行代码底层拆成三步分配一块内存空间在内存中初始化 Singleton 对象构造函数赋值将 instance 引用指向这块内存地址。这三步不存在数据依赖编译器 / CPU 可以重排顺序为1 → 3 → 2。多线程故障场景线程 A 执行 instancenew Singleton ()重排执行 1 分配内存、3 赋值引用还没执行初始化 2此时线程 B 进入判断if(instance null)发现 instance 不为空线程 B 直接使用 instance拿到一个未初始化完成的半空对象出现空指针 / 数据错乱。这就是指令重排序导致的并发 bug解决方案用volatile修饰 instance插入内存屏障禁止这三步重排。五、volatile 如何禁止指令重排序内存屏障 Memory Barriervolatile 会在读写前后插入对应屏障限制指令上下穿越屏障重排volatile 写操作StoreStore 屏障 StoreLoad 屏障 所有 volatile 写前面的读写不能重排到 volatile 写之后 volatile 写不能和后面的 volatile 读重排。volatile 读操作LoadLoad 屏障 LoadStore 屏障 volatile 读后面的读写不能重排到 volatile 读之前。简单记 写 volatile前面代码不许后移 读 volatile后面代码不许前移 彻底阻断跨 volatile 变量的指令乱序。六、补充synchronized 也能防止重排同步块整体具备原子性、有序性同步代码内部的指令不会跑到锁外面锁外代码也不会进入锁内天然屏蔽跨同步区的重排序。七、精简总结口述版指令重排序是 JIT 编译器、CPU、缓存系统为优化性能打乱代码执行顺序单线程遵循 as-if-serial 语义结果不变但多线程无保护时会出现逻辑错乱典型场景是 DCL 单例拿到未初始化对象volatile 通过插入内存屏障禁止跨 volatile 变量的重排序保证多线程下执行顺序符合代码书写逻辑。