在Java程序中,有时候需要采用延迟初始化来降低初始化类和创建对象的开销。双重检查锁定是常见的延迟初始化技术,但它是一个错误的用法。
比如,下面是非线程安全的延迟初始化对象的代码:
public static Instance getInstance() {
if (instance == null) { //1:线程A执行
instance = new Instance(); //2:线程B执行
}
return instance;
}
这种情况下,线程A在执行代码1的时候,线程B执行代码2,那么线程A就会看到instance引用还没有完成初始化。
所以为了解决这个问题就给getInstance
方法加上了synchronized
锁。
public synchronized static Instance getInstance() {
if (instance == null) {
instance = new Instance();
}
return instance;
}
但是synchronized
会导致性能开销,如果方法频繁地被多个线程调用,会导致程序执行性能下降。要是没有频繁调用,那还可以。
所以为了解决synchronized
巨大开销的问题,提出了“双重检查锁定(Double-checked Locking)”。
public static Instance getInstance() {
if (instance == null) { //1.第一次检查
synchronized(XXX.class) { //2.加锁
if (instance == null) { //3.第二次检查
instance = new Instance(); //出问题的根源
}
}
}
return instance;
}
当时这样就会有一个错误的优化,当线程执行第一行读取到instance不为null时,instance引用的对象有可能还没有完成初始化。
为什么会出现这种问题呢?
还要从instance = new Instance();
创建一个对象这行代码看起
这一行代码可以分解成如下3行伪代码:
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory) //2:初始化对象
instance = memory //3:设置instance指向刚分配的内存地址
本来是1-2-3的顺序执行,担忧有可能会被重排序,成1-3-2,当执行到instance = memory
的时候,还没有初始化对象呢。
而且最气人的是这个重排序并没有违反intra-thread semantics
。因为根据《The Java Language Specification,Java SE 7 Edition》所有线程在执行Java程序时必须遵守intra-thread semantics,它保证了重排序不会改变单线程内程序执行结果。
所以为了解决这个问题,我们就请出了volatile
关键字了,我们在学单例模型的时候,就会看到这样一句话:“啊 这个volatile可以防止重排序呢 用它!用它!”
只需要给对象加上volatile就好:private volatile static Instance instance;
但是为什么volatile可以防止重排序呢?
volatile通过插入读屏障和写屏障保证可见性,在volatile禁止重排序上,也是通过内存屏障实现的。
因为内存屏障可以使一些指令按照特定顺序执行。
volatile禁止指令重排序的规则:
1.当第二个操作是voaltile写时,无论第一个操作是什么,都不能进行重排序
2.当地一个操作是volatile读时,不管第二个操作是什么,都不能进行重排序
3.当第一个操作是volatile写时,第二个操作是volatile读时,不能进行重排序
除了加上volatile关键字,还有一种解决方法:基于类初始化
JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。
基于这个特性,可以实现另一种线程安全的延迟初始化方案Initialization On Demand Holder idiom
下面这张图是两个线程并发执行getInstance
方法的执行图:
这个方法的思想是:允许出现重排序,但是不允许非构造线程(这里指线程B)“看到”这个重排序。
之前出问题的根源是因为初始化过程的重排序被其他线程看到了,所以才会出错。这种类初始化阶段解决方法就是不让其他线程看到重排序。而volatile方法解决核心是不允许出现重排序