本篇文章想讲述的问题:
- 单例模式-双重检测锁为什么要用volatile关键字
- 上下代码有依赖关系时指令重排后的执行顺序
单例模式-双重检测锁为什么要用volatile关键字
先看代码:
public class Singleton {static Singleton instance;public Object o;public Singleton() {this.o = new Object();}static Singleton getInstance(){if (instance == null) {synchronized(Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}
假设现在有两个线程A和B,A现在先进入了第2个if判断并执行第14行代码对对象进行初始化。A线程进入到构造方法对对象进行初始化时,由于JVM会对指令进行重排,所以初始化的代码顺序可能是:
第一种顺序:
- 分配一块内存 M;
- 在内存 M 上初始化 Singleton 对象(对属性o进行初始化和赋值);
- 然后 M 的地址赋值给 instance 变量。
第二中顺序(实际上优化后的执行路径可能是这样的):
- 分配一块内存 M;
- 将 M 的地址赋值给 instance 变量;
- 最后在内存 M 上初始化 Singleton 对象(对属性o进行初始化和赋值)。
假设虚拟机对指令重排优化后,按第二种顺序执行。A线程现在执行到第二步将 M 的地址赋值给 instance 变量;现在cpu对线程切换并切换到B线程上,B此时若走到第一个if判断,此时instance已不为空,所以直接返回instance(此时A线程还没有完成对instance变量引用的对象完成初始化也没有执行到第16行对对象锁进行释放)。因为此时B返回的instance的属性o没有被初化或赋值,所以B线程返回的instance对象o属性为空,如果B线程对返回的instance对象的o属性进行方法调用可能出现空指针异常。
因此解决办法:对instance加上volatile关键字。
原因:volatile可以禁止JVM和处理器对指令进行重排。
volatile并不是可以禁止所有的重排,用以下代码来解释:
volatile int m;int a, b;long c, d;public void testVolatile() {a = 10;b = 10;m = 20;c = 30;d = 40;}
volatile能保证的顺序是:对volatile修饰的变量进行操作的语句的前面的语句,一定比对volatile修饰的变量进行操作的语句先执行。对volatile修饰的变量进行操作的语句的后面的语句,一定比对volatile修饰的变量进行操作的语句后执行。有点绕,简单说也就是:testVolatile方法被执行时,m = 20前面两行代码一定比m = 20要先执行(不能保证a = 10; b = 10;这两句的执行先后顺序);m = 20后面两行代码一定比m = 20要后执行(不能保证c = 30; d = 40;这两句的执行先后顺序)。
这就解释了为什么单例模式的双重检测锁为什么要用volatile关键字,以及volatile关键字是如何禁止指令重排的。
上下代码有依赖关系时指令重排后的执行顺序
代码示例:
public void method() {int a = 0;a = 10;int b = 0;b = 20;}
这个method方法,看上去可能是按写的代码的顺序执行的,执行顺序是:先执行第2行,再执行第3、4、5行。但是代码执行顺序,往往会被这个编译器优化,进行指令重排序。优化后的执行顺序可能是:先执行第4、5行,再执行第2、3行。也可能是:先执行第4、2行,再执行第3、5行。还有其它执行顺序的可能。但是有一点是确定的:因为第3行依赖第2行,所以第3行一定会在第2行后面执行,也就是第2行一定比第3行先执行;同理第4行也一定比第5行先执行;不然执行就会报错我有问题。
以上是个人观点,若又不正确的地方欢迎指正。