介绍 :
所谓的指令重排指的就是jvm在编译代码的时候 ,为了提高程序运行效率,在不影响单线程程序执行结果的前提下,对指令进行的排序,当然我们这里的是单线程,如果是在多线程中就会影响程序的结果了
可能你听了我的介绍 还是不明所以,到底什么是指令重排?,没关系,下面我们通过代码来理解到底什么是指令重排
1---> int a = 2 << 1;
2---> int b = 3 << 1;
3---> int result = a * b + 2333;
相信你看到这段简短的代码,也会知道这段代码的执行顺序,1 - > 2 -> 3,相信你会觉得这就是代码执行的顺序,但是在jvm中是不会这样执行的,这里我们可以用着三个变量的依赖关系来解释一下原因
这三个变量中 1 与 2没有依赖关系 3与 1和2 都有依赖关系,也就是说,没有依赖关系的两段代码即使我们将他们编译执行的顺序进行调换,这样也不会对代码的结果产生改变 也就是 上面的代码在jvm中实际上是 2 -> 1 -> 3
当然在单线程中JVM对代码进行指令重排并不会产生影响,但是在多线程中进行指令重排的话就会产生一些不确定的结果了,现在我们来看一下指令重排在多线程的一个经典的例子,单例模式,懒加载
public class DateBaseTools{
private static DateBaseTools instance = null;
public static DateBaseTools getSingInstance() {
if (instance == null) {
synchronized (DateBaseTools.class) {
instance = new DateBaseTools();
}
}
return instance;
}
public void action() {
System.out.println(">>>>>>>>>>");
}
}
再上面的懒加载模式中,我们要是在多线程并发中调用这个单例的话,就会因为JVM的指令重排造成一些不可预料的结果,下面我们来分析一下
看似简单的一段赋值语句: instance = new DateBaseTools();,但是很不幸它并不是一个原子操作,其实际上可以抽象为下面几条JVM指令:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
我们可以看出 第一条指令对第二条指令有依赖关系,但是第二条指令与第三条指令并没有依赖关系,所以根据JVM指令重排的规矩可以对第二条与第三条指令的执行顺序进行交换,这样看起来没有什么问题,但是在多线程中这样就会产生问题了
我们假定有两个线程,第一个线程调用单例模式的getSingInstance() 开始执行1 ->2 -> 3要是这个时候在JVM中执行的是 1 -> 3 ->2,也就是分配好内存空间后,为instance分配内存地址,这个时候线程二抢占cpu资源,执行getSingInstacne发现instance不为空 就会返回instance,这个时候返回的instanc还没进行初始化,肯定会报错了
解决方案
给单例类中引用的instance加上volatile关键字,volatile关键字有一个作用就是防止JVM对其进行指令重排序
在 volatile 变量的赋值操作后面会有一个内存屏障,大多数的处理器都支持内存屏障的指令。上面的代码在加上volatilc后getSingInstace操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。