双重检查锁定的由来
在多线程程序中,有时候需要采用延迟初始化来降低初始化类和创建对象的开销。首先提给出延迟出事化对象的示例代码:
public class Initialization{
private static Instance newInstance;
public static Instance getInstance(){
if(newInstance == null){ //1:A线程
newInstane = new Instance();//2:B线程
}
}
return newInstance;
}
如果在单线程环境下运行,这段代码是没有问题的,但是如果是这样就没有意义了。如果是在多线程环境下运行,A线程执行第1步的时候,B线程在执行第2步,此时B线程正在执行newInstance市里的初始化,并且没有将这个引用指向初始化的对象,或者对象根本就还没有初始化,A线程检查到的newInstance为null,那么A线程通过检查就会再次执行初始化的过程,这就违背了我们延迟初始化的初衷,无法达到我们预期的目标。
线程安全的延迟初始化
我们可以看到这是由于初始化对象的过程不是原子性的,所以导致上面的代码在多线程环境下的实际运行过程没能完成我们预期的目标。那么,我们可以考虑使用sychnorized来加锁,保证原子性,以达到线程安全的目的,代码如下:
public class Initialization{
private static Instance newInstance;
public sychnorized static Instance getInstance(){
if(newInstance == null){ //1:A线程
newInstane = new Instance();//2:B线程
}
return newInstance;
}
}
通过sychnorized关键字对getInstance()做了同步处理,看似解决了上面的问题,但又带来了另一个问题:sychnorized导致更多地性能开销。这将导致延迟初始化方案的性能在多线程环境下表现极差。
原始版本的双重检查锁定
借助于sychnorized,我们可以想到另一个巧妙地方法,利用双重检查保证每个线程都能正确的执行程序,达到预期的目标,代码如下:
public class Initialization{
private static Instance newInstance;
public static Instance getInstance(){
if(newInstance == null){ 1
sychnorized(DoubleCheckedLocking.class){ 2
if(newInstance == null){ 3
newInstance = new Instance(); 4
}
}
}
return newInstance;
}
}
表面上看起来,这是一个两全其美的方案:
- 多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个随想能创建对象。
- 在对象创建好之后,执行getInstance()方法不再需要获取锁,直接返回已创建好的对象。
看似解决方案已经完美,但是依然存在一个很致命的问题,这里首先需要分析一下对象初始化的过程:
- 分配对象的内存空间;
- 初始化对象;
- 将引用指向分配的内存地址。
由于编译器的重排序可能会将第2步和第3步重排序,将会导致一个严重的问题:加入A线程通过检查并且正在初始化对象,并且将2、3重排序,将导致B线程检查时发现引用不为null,那么他将会返回一个分配了内存地址却尚未完全初始化的对象。
了解了问题发生的根源后,可以想到以下解决的方案:
- 不允许2、3重排序;
- 允许2、3重排序,但不允许其他线程看到这个过程。
基于volatile的解决方案
volatile可以解决2、3重排序,保证返回的是完全初始化的对象,代码如下:
public class Initialization{
private volatile static Instance newInstance;
public static Instance getInstance(){
if(newInstance == null){ 1
sychnorized(DoubleCheckedLocking.class){ 2
if(newInstance == null){ 3
newInstance = new Instance(); 4
}
}
}
return newInstance;
}
}
基于类初始化的解决方案
还可以通过方案2来解决,主要根据类的初始化的原理,具体代码如下:
public class InstanceFactory{
private static class InstanceHolder{
public static Instance newInstance = new Instance();
}
public static Instance getInstance(){
return InstanceHolder.newInstance;
}
}
类加载过程中,类初始化会获取锁,这个锁可以同步多个线程对同一个类的初始化。详细原理推荐看《多线程并发编程艺术》java内存模型那一章有详细介绍。