一、什么是指令重排序
为了使处理器内部的运算单元能尽量被充分利用(多级缓存、多核处理器),处理器可能会对输入的代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果重组,并确保这一结果和顺序执行结果是一致的,但是这个过程并不保证各个语句计算的先后顺序和输入代码中的顺序一致。这就是指令重排序。
简单来说,就是指你在程序中写的代码,在执行时并不一定按照写的顺序。
指令重排序发生在编译和代码执行过程中。
二、指令重排序实践
public class VolatileReOrderSample {
//定义四个静态变量
private static int x=0,y=0;
private static int a=0,b=0;
public static void main(String[] args) throws InterruptedException {
int i=0;
while (true){
i++;
x=0;y=0;a=0;b=0;
//开两个线程,第一个线程执行a=1;x=b;第二个线程执行b=1;y=a
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
//线程1会比线程2先执行,因此用nanoTime让线程1等待线程2 0.01毫秒
shortWait(10000);
a=1;
x=b;
}
});
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
b=1;
y=a;
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
//等两个线程都执行完毕后拼接结果
String result="第"+i+"次执行x="+x+"y="+y;
//如果x=0且y=0,则跳出循环
if (x==0&&y==0){
System.out.println(result);
break;
}else{
System.out.println(result);
}
}
}
//等待interval纳秒
private static void shortWait(long interval) {
long start=System.nanoTime();
long end;
do {
end=System.nanoTime();
}while (start+interval>=end);
}
}
如果程序按照代码顺序执行,thread1和thread2执行的指令分别如下
thread1
第一步:对a赋值1
第二步:取b的值
第三步:将b的值赋给x
thread2
第一步:对b赋值1
第二步:取a的值
第三步:将a的值赋给y
如果每个线程执行的指令按照书写顺序执行,那么我们可能得到这些结果:
x = 0, y = 1;
x = 1, y = 0;
x = 1, y = 1;
但是无论如何都不可能得到 x = 0, y = 0
然而事实是,这个程序最终输出了x = 0, y = 0 这一结果。出现这一结果的原因是:
thread1和thread2的第二步和第三步被重排序到了第一步之前。
三、如何禁止指令重排序
内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。
内存屏障可以被分为以下几种类型
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。 在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
有的处理器的重排序规则较严,无需内存屏障也能很好的工作,Java编译器会在这种情况下不放置内存屏障。
为了保证final字段的特殊语义,也会在下面的语句加入内存屏障。
x.finalField = v; StoreStore; sharedRef = x;
volatile关键字禁止指令重排序的底层原理就是内存屏障,上述例子中,如果我们把 x 声明为 volatile,那么thread1的第三步之前的操作就不可能被重排序到之后。
四、happens before原则
指令重排序并不是毫无约束的,需要遵守一些原则。这就是happens before法则。
happens before法则保证一个线程的某些特定操作对其他线程操作的可见性。
Happens before对所有操作定义了一个局部顺序。为了保证执行Y操作的线程可以看到操作X的结果(无论X和Y是否在同一个线程中执行),X和Y之间必须存在一个先后顺序,在没有happens before法则之前,JVM可以按需随意地重排序他们。
Happens before法则包含以下六条:
- 单线程次序法则:单线程中的操作按照编程顺序,前面的操作先于后面的操作执行。(如果前后操作不具有依赖性,可重排序)
- 监控锁法则:对一个监控锁(synchronized)的unlock操作,先于后续的获取锁操作。
- volatile法则:写volatile变量之前的操作,不能重排序到之后;读volatile之后的操作,不能重排序到之前。
- 线程启动法则:Thread.start()方法永远先于线程中run()方法内的操作。
- 线程join法则:Thread A中执行了ThreadB.join(),那么Thread B中的所有操作先于Thread A中的所有操作。
- 线程中断法则:线程中断一定发生在另一个线程检测到中断状态之前。
- 对象析构法则:一个对象的构造函数结束一定发生在对象finalize方法调用之前。
- 传递性法则:A 先于B, B先于C,那么A一定先于C执行