首先一定要明确:指令重排序和有序性是不一样的。这一点非常重要。
我们经常都会这么说:
①、volatile能保证内存可见性、禁止指令重排序但是不能保证原子性。
②、synchronized能保证原子性、可见性和有序性。
注意:这里的有序性并不是代表能禁止指令重排序。
下面看一个非常典型的禁止重排优化的例子DCL,如下:
在双重检查的单例模式中,既然已经加了synchronized为什么还需要volatile去修饰变量呢?如果synchronized能禁止指令重排,那么完全可以不用要volatile。
package com.cctv; public class DoubleCheckLock { private static DoubleCheckLock instance; private DoubleCheckLock() { } public static DoubleCheckLock getInstance() { //第一次检测 if (instance == null) { //同步 synchronized (DoubleCheckLock.class) { if (instance == null) { //多线程环境下可能会出现问题的地方 instance = new DoubleCheckLock(); } } } return instance; } }
上述代码一个经典的单例的双重检测的代码,这段代码在单线程环境下并没有什么问题,但如果在多线程环境下就可以出现线程安全问题。
原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。
因为instance = new DoubleCheckLock();这句代码并不是一个原子操作,他分为三步(伪代码)
memory = allocate();//1.分配对象内存空间 instance(memory);//2.初始化对象 instance = memory;//3.设置instance指向刚分配的内存地址,此时instance!=null
由于步骤2和步骤3间可能会重排序,如下:
memory = allocate();//1.分配对象内存空间 instance = memory;//3.设置instance指向刚分配的内存地址,此时instance!=null instance(memory);//2.初始化对象
由于步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
那么该如何 解决呢,很简单,我们使用volatile禁止instance变量被执行指令重排优化即可
//禁止指令重排优化 private volatile static DoubleCheckLock instance;