懒加载中的DCL

java中经常会使用延迟一些高开销对象的初始化过程,等到使用再加载,称之为懒加载。

单例中懒汉式

下面时单例中的懒汉式代码。

1
2
3
4
5
6
public static Instance getInstance(){
if(instance==null){ //1
instance=new Instance(); //2
}
retrun instance; //3
}

这代码子是典型的 check-then-act的模式,在多线程情况下存在多线程安全问题。例如线程1执行完步骤1还未执行2时,让出执行权。线程2进来会将instance初始化。线程1唤醒时会再次初始化对象,破坏了单例。

DCL实现懒汉式

为了解决线程安全问题,getInstance方法要加锁。但是根据getInstance方法的作用。锁再instance对象初始化完毕后就没作用。 所以采用DCL (Double-Checked Locking)双层检查锁的方式。通过第一次检查如果instance初始化了就直接跳过加锁代码,返回instance。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Instance{
private Instance(){};
private static Instance instance;
public static Instance getInstance(){
if(instance==null){ //1
synchronized(lock){
if(instance==null){ //2
instance=new Instance(); //3
}

}
}
return instance; //4
}
}

DCL实现懒汉式存在问题

这种写法看上去解决了线程安全,也兼顾了效率。其实不然,这种代码会导致getInstanceh返回一个没有初始化完成的Instance对象。

问题出现在第三步,instance=new Instance()这行代码可以用下面三行伪代码表示

1
2
3
menory=allocate();    //1.分配内存
ctorInstance(menory); //2.初始化
instance=menory; //3.赋值给instance

intra-thread semantics(线程内语义)允许那些在单线程内,不会改变单线程程序执行结果的重排序。重排序后的顺序:

1
2
3
menory=allocate();    //1.分配内存
instance=menory; //2.赋值给instance
ctorInstance(menory); //3.初始化

对单线程来说只要Instance初始化在该线程访问Instance对象之前都是不会改变执行结果。

注意:

1
happens-before的锁定规则是“一个unLock操作先行发生于后面对同一个锁的lock操作”并不会禁止锁内部的重排序。

在这种情况下,如果线程1执行步骤2 instance=menory后,线程2获取执行权判断if(instance==null)时,instance不为null,线程2将会返回一个未初始化完成的instance对象。

执行顺序 线程1 线程2
1 分配内存
2 设置instance指向内存空间
3 判断instance是否为null
4 初次访问Instance对象
5 初始化Instance对象
6 初次访问Instance对象

解决方法

使用volatile+DCL

该问题产生的原因是2和3的重排序导致的。

1
2
3
menory=allocate();    //1.分配内存
instance=menory; //2.赋值给instance
ctorInstance(menory); //3.初始化

那么解决方案可以是禁止2和3的重排序。

通过将instance变量设置为volatile变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Instance{
private Instance(){};
private volatile static Instance instance;
public static Instance getInstance(){
if(instance==null){
synchronized(lock){
if(instance==null){
instance=new Instance();
}

}
}
return instance;
}
}

volatile变量会通过内存屏障来禁止重排序。

通过类加载实现

jvm类加载后,执行类初始化时,会去获取锁同步多线程初始化类。

1
2
3
4
5
6
7
8
class InstanceFactory(){
private InstanceHolder(){
public final static Instance instance= new Instance();
}
public Instance getInstance(){
return InstanceHolder.instance;
}
}

执行图

image

这里初始化过程虽然也发生了重排序,但是对于线程2来说这个操作是原子性的。线程2只能看到操作未开始或者已经结束之后。