文章目录
什么是重排序?
重排序是指在不改变程序最终执行结果的前提下,编译器、处理器或内存系统对指令的执行顺序进行重新排列,以提高程序的执行效率。
重排序的三种来源
- 编译器重排序:
- 编译器在生成字节码或机器码时,可能会对指令进行重新排序。
- 例如:将不相关的指令提前或延后执行,以减少指令流水线的停顿。
- 处理器重排序:
- 现代处理器为了提高执行效率,可能会对指令进行乱序执行(Out-of-Order Execution)。
- 例如:处理器可能会将没有数据依赖的指令并行执行。
- 内存系统重排序:
- 由于 CPU 缓存的存在,不同线程对内存的读写操作可能会被重新排序。
- 例如:写操作可能会被延迟,而读操作可能会被提前。
重排序的影响
在单线程环境下,重排序不会影响程序的最终结果,因为编译器和处理器会保证程序的 串行语义(As-If-Serial)。但在多线程环境下,重排序可能会导致 可见性问题 和 有序性问题,从而引发并发 Bug。
可见性问题
- 由于重排序,一个线程对共享变量的修改可能对其他线程不可见。
例如:
int a = 0;
boolean flag = false;
// 线程 1
a = 1; // 操作 1
flag = true; // 操作 2
// 线程 2
if (flag) { // 操作 3
System.out.println(a); // 操作 4
}
- 由于重排序,操作 1 和操作 2 可能会被重新排序,导致线程 2 看到
flag
为true
,但a
仍然是0
。
注意:两个线程是并发执行的,但并不是完全同时执行的。对于并发执行的线程,操作的顺序是不可预知的,而这正式引发重排序问题的原因
有序性问题
- 由于重排序,程序的执行顺序可能与代码的编写顺序不一致。
例如:
int x = 0;
int y = 0;
// 线程 1
x = 1; // 操作 1
y = 2; // 操作 2
// 线程 2
if (y == 2) { // 操作 3
System.out.println(x); // 操作 4
}
- 由于重排序,操作 1 和操作 2 可能会被重新排序,导致线程 2 看到
y
为2
,但x
仍然是0
。
如何避免重排序带来的问题?
为了避免重排序带来的并发问题,Java 提供了 内存屏障(Memory Barrier) 和 同步机制 来保证程序的可见性和有序性。
volatile关键字
volatile
保证每次读取和写入共享变量时,都会直接访问主内存,而不是线程的本地缓存,因此它确保了变量在多个线程之间的 可见性。
例如:
volatile boolean flag = false;
- 当
flag
被标记为volatile
后,线程 A 对flag
的修改会立即对线程 B 可见,不会存在缓存问题。
synchronized 关键字
synchronized
关键字通过加锁和解锁操作来保证代码块的原子性、可见性和有序性。
例如:
synchronized (lock) {
a = 1;
flag = true;
}
final关键字
final
关键字可以禁止构造函数中的指令重排序,确保对象的正确初始化。
例如:
final int x;
x = 10; // 构造函数中的赋值操作不会被重排序
happens-before规则
- Java 内存模型(JMM)定义了一系列 happens-before 规则,用于描述操作之间的可见性和有序性。
- 例如:
- 单线程规则:在同一个线程中,前面的操作 happens-before 后面的操作。
volatile
规则:对volatile
变量的写操作 happens-before 后续对它的读操作。- 锁规则:解锁操作 happens-before 后续的加锁操作。