线程安全的延迟初始化:线程安全的创建单例
2019-12-31 本文已影响0人
天冷请穿衣
2019-12-31
为什么要进行字段的延迟初始化?
因为对字段的延迟初始化可以降低初始化类或创建实例的开销。
不安全的双重检查加锁延迟初始化方案
public class DoubleCheckd {
private DoubleCheckd instance;
public DoubleCheckd getInstance(){
if(instance == null){//1
synchronized(DoubleCheckd .class){//2
if(instance == null){//3
instance = new DoubleCheckd();//4
}
}
}
return instance;//5
}
}
这个方案是线程不安全的,问题是由于代码行4引起的。因为4实际的动作会分成下面的三个步骤:
a.在堆内存中开辟所需要的空间;
b.初始化对象;
c.将分配的空间地址赋值给引用变量。
由于为了提高代码的执行性能,编译器和系统都会进行重排序,因此上述这三个步骤不一定是按照上面的顺序执行,b和c可能被重排序,即b先于c被执行。
在单线程的情况下并不会出现任何问题的,但在多线程的情况就可能会有问题,因为当线程A刚完成步骤c但未执行b时,若恰好有线程B运行到1处,那么此时线程B就会获取到一个不为null但却未被初始化的实例对象,其后续的操作将发生不可预知的错误。
知道了造成问题的原因解决方法也就出来了。因此只有解决如下两个问题即可:
1)避免b和c的重排序
2)使c操作不对外可见
安全的延迟初始化方式
1. 基于volatile的双重检查加锁方式
public class DoubleCheckd {
private volatile DoubleCheckd instance;
public DoubleCheckd getInstance(){
if(instance == null){//1
synchronized(DoubleCheckd .class){//2
if(instance == null){//3
instance = new DoubleCheckd();//4
}
}
}
return instance;//5
}
}
此优化方法很简单,就是在原来的基础上加上volatile修饰instance变量,因为根据JMM的规则,volatile能够禁止b、c的重排序。
ps:本来曾想过使用initFlag==true代替instance==null,在代码4下面加上initFlag=true以避免b和c的重排序造成的问题,但实际上也是没用的,因为initFlag=true也可能会重排序到4前面。还是会存在使用未初始化完成的对象的危险。
2. 基于类初始化的延迟方式
public class ClassInitialization {
private static class InitialInstanceClass{
private static ClassInitialization instance = new ClassInitialization();//1
}
public static ClassInitialization getInstance(){
return InitialInstanceClass.instance;//2
}
}
该方式是利用多线程同时加载一个类时,jvm已保障了只有一个线程能够对该类进行初始化,从而保证了初始化的安全性。也即在多线程情况下,有且只有一个线程能够执行代码1,因此即使代码执行时出现重排序也不会被其它线程看见。
若要对实例字段延迟初始化可以采用方式1,若要对静态字段延迟初始化可以采用方式2。