什么是指令重排序
指令重排序(Instruction Reordering)是指编译器和处理器为了优化程序性能,在不改变单线程程序执行结果的前提下,对指令的执行顺序进行调整。
核心特点
- 单线程语义保证:重排序不会改变单线程的执行结果
- 多线程问题:重排序可能导致多线程程序出现意外行为
- 透明性:重排序对程序员是透明的(单线程下)
为什么会发生重排序
1. 性能优化的需求
现代CPU为了提高执行效率,采用了多种优化技术:
指令执行流水线:
┌─────────┬─────────┬─────────┬─────────┬─────────┐
│ 取指令 │ 译码 │ 执行 │ 访存 │ 写回 │
└─────────┴─────────┴─────────┴─────────┴─────────┘
优化目标:
1. 充分利用流水线(避免流水线停顿)
2. 减少缓存未命中
3. 提高指令并行度
2. 重排序示例
public class ReorderingExample {
int a = 0;
boolean flag = false;
// 线程1
public void writer() {
a = 1; // 1. 写a
flag = true; // 2. 写flag
}
// 线程2
public void reader() {
if (flag) { // 3. 读flag
int i = a; // 4. 读a
// 可能读到a=0!
}
}
}
可能的执行顺序(重排序后):
原始顺序:1 → 2 → 3 → 4
重排序后:2 → 1 → 3 → 4
结果:flag=true时,a可能还是0
重排序的三个层次
1. 编译器重排序
编译器优化:在编译阶段调整指令顺序
// 原始代码
int a = 1;
int b = 2;
int c = a + b;
// 编译器可能重排为
int b = 2; // 先计算b
int a = 1; // 再计算a
int c = a + b;
Java字节码层面:
public void method() {
int x = 1;
int y = 2;
int z = x + y;
}
// 字节码可能重排序
0: iconst_2 // 先加载2
1: istore_2 // 存储y
2: iconst_1 // 再加载1
3: istore_1 // 存储x
4: iload_1
5: iload_2
6: iadd
7: istore_3 // 存储z
2. 处理器重排序
指令级并行(ILP)
// CPU可能并行执行的指令
public void execute() {
int a = 1; // 指令1
int b = 2; // 指令2(不依赖指令1,可并行)
int c = 3; // 指令3(不依赖指令1、2,可并行)
int d = a + b; // 指令4(依赖1、2,必须等待)
}
乱序执行(Out-of-Order Execution)
指令流水线:
时刻T1: [指令1取指] [指令2译码] [指令3执行] [指令4访存] [指令5写回]
如果指令2需要等待内存数据,CPU会先执行指令3、4
3. 内存系统重排序
写缓冲区(Store Buffer)
CPU核心:
↓ 写入
Store Buffer(写缓冲区)
↓ 延迟刷新
L1 Cache
↓
L2 Cache
↓
主内存
问题:写入Store Buffer后,CPU认为写入完成,但实际未刷新到主内存
public class StoreBufferReordering {
int a = 0;
int b = 0;
// CPU1
public void cpu1() {
a = 1; // 写入Store Buffer,未刷新到内存
int r1 = b; // 读取b(可能还是0)
}
// CPU2
public void cpu2() {
b = 1; // 写入Store Buffer,未刷新到内存
int r2 = a; // 读取a(可能还是0)
}
// 可能结果:r1=0, r2=0(两个CPU看到的写入顺序不同)
}
重排序的类型
1. LoadLoad重排序
int a = x; // Load1
int b = y; // Load2
// 可能重排为
int b = y; // Load2先执行
int a = x; // Load1后执行
2. StoreStore重排序
x = 1; // Store1
y = 2; // Store2
// 可能重排为
y = 2; // Store2先执行
x = 1; // Store1后执行
3. LoadStore重排序
int a = x; // Load
y = 1; // Store
// 可能重排为
y = 1; // Store先执行
int a = x; // Load后执行
4. StoreLoad重排序
x = 1; // Store
int a = y; // Load
// 可能重排为
int a = y; // Load先执行
x = 1; // Store后执行
// 这是开销最大的重排序,需要完整的内存屏障
重排序带来的问题
问题1:双重检查锁的重排序
public class Singleton {
private static Singleton instance; // 没有volatile
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 问题在这里!
}
}
}
return instance;
}
}
问题分析:
instance = new Singleton();
// 这行代码实际包含3个步骤:
1. memory = allocate(); // 分配内存空间
2. ctorInstance(memory); // 初始化对象
3. instance = memory; // 设置instance指向内存地址
// 可能被重排序为:
1. memory = allocate(); // 分配内存
3. instance = memory; // 指向内存(对象还未初始化)
2. ctorInstance(memory); // 初始化对象
// 线程A执行到步骤3时,线程B进入getInstance()
// 线程B发现instance != null,直接返回instance
// 但此时对象还未初始化完成!→ 使用半初始化对象 → 出错
解决方案:
public class Singleton {
private static volatile Singleton instance; // 使用volatile
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // volatile禁止重排序
}
}
}
return instance;
}
}
问题2:无锁发布对象
public class UnsafePublication {
private Object resource;
private boolean initialized = false;
// 线程1:初始化
public void init() {
resource = new Object(); // 1. 创建对象
initialized = true; // 2. 设置标志
// 可能重排序为:
// 2 → 1,导致initialized=true时,resource还是null
}
// 线程2:使用
public void use() {
if (initialized) {
resource.doSomething(); // NullPointerException!
}
}
}
解决方案:
public class SafePublication {
private Object resource;
private volatile boolean initialized = false; // volatile
public void init() {
resource = new Object();
initialized = true; // volatile写,禁止之前的操作重排序到之后
}
public void use() {
if (initialized) { // volatile读,禁止之后的操作重排序到之前
resource.doSomething(); // 保证能看到完全初始化的resource
}
}
}
禁止重排序的机制
1. 内存屏障(Memory Barrier)
内存屏障是CPU提供的指令,用于禁止特定类型的重排序。
| 屏障类型 | 指令 | 说明 |
|---|---|---|
| LoadLoad | Load1; LoadLoad; Load2 | 禁止Load1与Load2重排序 |
| StoreStore | Store1; StoreStore; Store2 | 禁止Store1与Store2重排序 |
| LoadStore | Load1; LoadStore; Store2 | 禁止Load1与Store2重排序 |
| StoreLoad | Store1; StoreLoad; Load2 | 禁止Store1与Load2重排序(最强) |
2. volatile的内存屏障
class VolatileExample {
int a = 0;
volatile boolean flag = false;
// 写线程
public void writer() {
a = 1; // 普通写
// ↓ StoreStore屏障
flag = true; // volatile写
// ↓ StoreLoad屏障
}
// 读线程
public void reader() {
// ↓ LoadLoad屏障
if (flag) { // volatile读
// ↓ LoadStore屏障
int i = a; // 普通读
}
}
}
JMM的插入策略:
在volatile写之前:
- 插入StoreStore屏障:禁止之前的普通写与volatile写重排序
在volatile写之后:
- 插入StoreLoad屏障:禁止volatile写与之后的读写重排序
在volatile读之后:
- 插入LoadLoad屏障:禁止volatile读与之后的读重排序
- 插入LoadStore屏障:禁止volatile读与之后的写重排序
3. happens-before规则
JMM通过happens-before规则定义操作之间的偏序关系:
// 规则1:程序顺序规则
a = 1;
b = 2;
// a = 1 happens-before b = 2(单线程内)
// 规则2:volatile变量规则
volatile boolean flag;
// flag的写 happens-before flag的读
// 规则3:传递性
a = 1;
flag = true; // volatile写
if (flag) { // volatile读
int i = a;
}
// a = 1 happens-before flag写
// flag写 happens-before flag读
// flag读 happens-before a的读
// 传递:a = 1 happens-before a的读
// 规则4:锁规则
synchronized (lock) {
a = 1;
} // 解锁 happens-before 下一次加锁
// 规则5:线程启动规则
Thread t = new Thread(() -> {
int a = x; // 能看到主线程在start前的所有操作
});
t.start(); // start() happens-before 线程内任何操作
// 规则6:线程终止规则
t.join(); // join返回 happens-before 线程内的所有操作
// 规则7:线程中断规则
t.interrupt(); // interrupt() happens-before 检测到中断
4. final字段的重排序规则
public class FinalFieldExample {
final int x;
int y;
static FinalFieldExample obj;
public FinalFieldExample() {
x = 1; // final字段的写
y = 2; // 普通字段的写
}
// 线程1
public static void writer() {
obj = new FinalFieldExample();
}
// 线程2
public static void reader() {
FinalFieldExample o = obj;
if (o != null) {
int a = o.x; // 保证能看到x=1
int b = o.y; // 不保证能看到y=2
}
}
}
final的重排序规则:
- 写final字段:禁止”final字段写”与”构造函数返回”重排序
x = 1; // final写 // ↓ StoreStore屏障 return this; // 构造函数返回 - 读final字段:禁止”读对象引用”与”读final字段”重排序
obj = new FinalFieldExample(); // 读引用 // ↓ LoadLoad屏障 int a = obj.x; // 读final字段
实战案例
案例1:无锁单例模式(Holder模式)
public class Singleton {
private Singleton() {}
// 静态内部类
private static class Holder {
// final保证完全初始化
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE; // 类加载机制保证线程安全
}
}
原理:
- 类加载时,JVM保证初始化的线程安全性
final保证INSTANCE完全初始化后才对其他线程可见
案例2:无锁发布对象
public class SafePublication {
private final Object resource;
public SafePublication() {
this.resource = new Object(); // final字段
}
// 不需要volatile或synchronized
public Object getResource() {
return resource; // final保证可见性
}
}
案例3:Disruptor的序列号屏障
public class Sequence {
private volatile long value = -1; // volatile保证可见性和有序性
public void set(long value) {
this.value = value;
}
public long get() {
return value;
}
// Disruptor使用volatile + CAS实现高性能队列
}
性能影响
内存屏障的开销
// 性能测试
public class BarrierOverhead {
int normalVar = 0;
volatile int volatileVar = 0;
@Benchmark
public void testNormal() {
normalVar++; // 约1ns
}
@Benchmark
public void testVolatile() {
volatileVar++; // 约10-20ns(有内存屏障开销)
}
}
开销排序:
StoreLoad(最大) > StoreStore ≈ LoadStore ≈ LoadLoad
答题总结
核心要点
-
定义:指令重排序是编译器和处理器为优化性能,在不改变单线程结果的前提下调整指令执行顺序
- 三个层次:
- 编译器重排序(编译时)
- 处理器重排序(运行时,指令级并行)
- 内存系统重排序(缓存、写缓冲区)
-
四种类型:LoadLoad、StoreStore、LoadStore、StoreLoad
- 禁止重排序:
- volatile(内存屏障)
- synchronized/Lock(锁规则)
- final(初始化安全)
- happens-before规则
- 典型问题:双重检查锁的半初始化对象
面试答题模板
“指令重排序是编译器和处理器为了提高程序性能,在保证单线程语义不变的前提下,对指令的执行顺序进行调整。
重排序有三个层次:一是编译器重排序,在编译阶段调整指令顺序;二是处理器重排序,CPU通过乱序执行和流水线优化提高并行度;三是内存系统重排序,由于写缓冲区和缓存导致的内存访问顺序变化。
重排序在单线程下没问题,但多线程下可能导致意外行为。典型例子是双重检查锁的单例模式,对象创建包括分配内存、初始化对象、赋值引用三步,可能被重排序为分配内存、赋值引用、初始化对象,导致其他线程拿到未完全初始化的对象。
解决方案是使用volatile关键字。volatile通过插入内存屏障,禁止特定类型的重排序。volatile写之前插入StoreStore屏障,写之后插入StoreLoad屏障;volatile读之后插入LoadLoad和LoadStore屏障。这样保证了volatile变量前后的操作不会被重排序。
此外,synchronized、final字段、happens-before规则也都能禁止重排序,保证多线程的正确性。”