Skip to content

指令重排序

代码写在前面的,真的会先执行吗?

在并发编程里,答案是不一定。编译器、CPU、缓存都可能「悄悄」调整执行顺序,这就是指令重排序

三种重排序类型

类型发生在哪里影响
编译器重排序JVM/JIT 编译器改变代码逻辑(单线程内无害)
指令级重排序CPU 流水线改变指令执行顺序
内存重排序CPU 缓存改变内存可见顺序

as-if-serial 语义

在不改变单线程程序执行结果的前提下,编译器和处理器可以任意重排序。

java
// 单线程下,无论如何重排序,c 都是 3
int a = 1;
int b = 2;
int c = a + b;

问题在于:多线程环境下,这种优化可能暴露 bug

重排序导致的问题

java
public class ReorderingProblemDemo {

    private static int a = 0, b = 0, x = 0, y = 0;

    public static void main(String[] args) throws InterruptedException {
        int count = 0;

        for (int i = 0; i < 10000; i++) {
            a = 0; b = 0; x = 0; y = 0;
            count++;

            Thread t1 = new Thread(() -> {
                a = 1;
                x = b;
            });

            Thread t2 = new Thread(() -> {
                b = 1;
                y = a;
            });

            t1.start();
            t2.start();
            t1.join();
            t2.join();

            // 理论分析:
            // 如果没有重排序,不可能出现 (x=0, y=0)
            // 因为至少有一个线程先执行
            // 但如果发生了内存重排序,可能出现 (x=0, y=0)
            if (x == 0 && y == 0) {
                System.out.println("检测到重排序导致的问题!x=0, y=0,循环次数: " + count);
                break;
            }
        }

        if (count >= 10000) {
            System.out.println("未检测到问题(但重排序风险仍然存在)");
        }
    }
}

这个实验可能检测到 (x=0, y=0) 的组合,因为 CPU 的内存重排序。

happens-before 规则:多线程中的「秩序」

JMM 定义了 happens-before 规则,在满足规则的情况下,禁止重排序

┌──────────────────────────────────────────────────┐
│              happens-before 禁止的重排序            │
├──────────────────────────────────────────────────┤
│                                                  │
│  1. volatile 写 HB volatile 读                    │
│     ┌────────┐         ┌────────┐               │
│     │ volatile │ ──────► │ volatile │               │
│     │  写     │   HB    │  读     │               │
│     └────────┘         └────────┘               │
│     写操作不能重排序到读之后                        │
│                                                  │
│  2. synchronized:unlock HB lock                 │
│     ┌────────┐         ┌────────┐               │
│     │ unlock  │ ──────► │  lock   │               │
│     └────────┘         └────────┘               │
│     前一个线程的写,对后一个线程可见               │
│                                                  │
│  3. 程序顺序规则:单线程内前 HB 后                │
│     a = 1;                                       │
│     b = 2;                                       │
│     // a = 1 不能重排序到 b = 2 之后             │
│                                                  │
└──────────────────────────────────────────────────┘

锁与顺序性

synchronized 块内的操作不会被重排序到块外:

java
public class LockOrderingDemo {

    private static int a = 0;
    private static int b = 0;
    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                a = 1;
                b = 2;
                // synchronized 内部的指令不能重排序到外部
                // 进入时刷新缓存,退出时写回主内存
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                int x = a;
                int y = b;
                System.out.println("x=" + x + ", y=" + y);
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

volatile 防止重排序

java
public class VolatileReorderDemo {

    // 普通变量:可能重排序
    private int number = 0;
    private boolean ready = false;

    // volatile 变量:禁止重排序
    private volatile int volNumber = 0;
    private volatile boolean volReady = false;
}

volatile 的内存屏障:

volatile 写之前:  StoreStore 屏障(禁止之前的写被重排序到屏障后)
volatile 写:      StoreLoad  屏障(强制写刷新到主内存)
volatile 读之后:  LoadLoad   屏障(禁止之后的读被重排序到屏障前)
                   LoadStore  屏障(禁止之后的写被重排序到屏障前)

单例模式与重排序

这是最经典的重排序问题:

java
public class SingletonReorder {

    // ❌ 危险示例:可能返回未完全初始化的对象
    private static SingletonReorder instance;

    public static SingletonReorder getInstanceWrong() {
        if (instance == null) {
            synchronized (SingletonReorder.class) {
                if (instance == null) {
                    instance = new SingletonReorder();
                    // new SingletonReorder() 实际执行:
                    // 1. 分配内存
                    // 2. 调用构造函数
                    // 3. 将引用赋值给 instance
                    // 如果 2 和 3 重排,其他线程可能看到未初始化的对象
                }
            }
        }
        return instance;
    }

    // ✅ 正确示例:volatile 防止重排序
    private static volatile SingletonReorder instance2;

    public static SingletonReorder getInstanceRight() {
        if (instance2 == null) {
            synchronized (SingletonReorder.class) {
                if (instance2 == null) {
                    instance2 = new SingletonReorder();
                    // volatile 防止重排序:
                    // 1. 分配内存
                    // 2. 调用构造函数
                    // 3. 将引用赋值给 instance
                    // 顺序固定,结果一致
                }
            }
        }
        return instance2;
    }

    private SingletonReorder() {}
}

数据依赖性

如果两个操作之间有数据依赖关系,编译器不能重排序:

写后读: x = 1; y = x;    ← 不能重排序(y 依赖 x)
写后写: x = 1; x = 2;    ← 不能重排序(后一个写覆盖前一个)
读后写: y = x; x = 2;    ← 不能重排序(x 的写依赖 x 的读)

跨线程的数据依赖,编译器无法感知——这就是并发 bug 的根源。

重排序对比表

机制防止自身重排序防止对其他操作的重排序
volatile✅ 自身✅ 前后操作
synchronized✅ 块内所有操作
final✅ 安全发布后✅ 安全发布后
程序顺序✅ 单线程内-

注意事项

  1. 单线程内重排序无害:as-if-serial 保证正确性
  2. 跨线程需要 happens-before:否则可能出现奇怪的结果
  3. volatile 防止重排序:不仅保证可见性,也保证有序性
  4. synchronized 防止重排序:进入刷新缓存,退出写回缓存
  5. 数据依赖性只在单线程内有效:多线程间无意义

要点回顾

  • 重排序有三种:编译器、指令级、内存重排序
  • as-if-serial 只对单线程有效
  • happens-before 规则限制了多线程间的重排序
  • volatile 通过内存屏障防止重排序
  • synchronized 块内的操作不会被重排序到块外
  • 单例模式中 volatile 防止重排序导致访问未初始化对象

相关链接

基于 VitePress 构建