指令重排
指令重排(HappenBefore): 执行代码的顺序可能和编写代码的顺序不一致,即虚拟机优化代码顺序,则为指令重排。即编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。
-
在虚拟机层面:为了尽可能的减少内存操作速度远远慢于cpu执行速度所带来的cpu空闲的影响,虚拟机会按照自己的一些规则将程序编写顺序打乱,即先写的可能后执行,后写的可能先执行。
-
在硬件层面,cpu会将接受到的一批指令按照其规则重排序,同样是基于cpu速度比缓存速度快的原因,和上面的目的相似。只是cpu每次只能在有限的范围类重排,而虚拟机可以在更大层面,更多指令范围内重排。
例如在机器中,代码的执行分4步:
- 获取指令
- 解码,翻译指令,从寄存器中取值
- 操作
- 将结果写回寄存器中
如:subTotal = price + fee;
total +=subTotal;
isDone = true;
price + fee这条语句在计算的过程可能比较慢,在结果写回之前发现isDone和计算过程毫不相干,为了提升性能,就先执行了isDone=true;等subTotal写回后再执行total+=subTotal语句。这在单线程中是没问题的。但在多线程,如果包含isDone的条件语句中有修改total的值的操作,结果就会发生变化。
数据依赖 如果两个操作访问同一个变量,且这两个操作中一个为写操作,此时这两个操作之间就存在数据依赖。
数据依赖分为三种类型:
- 先写后读 a=2;b=a;
- 先读后写 a=b;b=2;
- 先写后写 a=2;a=1;
上面三种情况中,只要重排了两个操作的顺序,程序执行的结果将会改变。
而编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。
这里是一个java程序实例:
package Ohter;
/**
* 指令重排:代码执行顺序与预期不一致
* 目的:提高性能
* @author CR553
*
*/
public class HappenBefore {
public static int a=0;
public static boolean flag=false;
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<10;i++)
{
a=0;
flag=false;
//更改数据
Thread t1=new Thread(()->{
a=1;
flag=true;
});
//读取数据
Thread t2=new Thread(()-> {
if(flag)//按道理讲如果这条语句执行了,就不会执行下面的输出语句了
{
a*=1;
}
//指令重排发生了!jvm没有等待a*=1;运行完就开始运行下一个语句,导致输出了a的值
if(a==0)
{
System.out.println("happenBefore a-->"+a);//a的值可能为0或1 为0好理解;为1的原因是在执行if(a==0)结束后a*=1才结束了,导致输出的是1
}
});
t1.start();
t2.start();
t1.join();//保证数据一定先在读取前修改
t2.join();
}
}
}