双重检查锁与延迟初始化
双重检查锁的错误根源?
背景:在Java多线程程序中,有时我们需要采用延迟初始化来降低初始化类和创建对象的开销,双重检查锁是常见的延迟初始化技术,但是一个错误的用法。原因?
- 非线程安全的延迟初始化示例:
public class UnsafeLazyInitialization {
private static ConfigContext instance;
public static ConfigContext getInstance() {
if (instance == null) {
instance = new ConfigContext();
}
return instance;
}
}
- 使用同步实现线程安全的延迟初始化:
public class UnsafeLazyInitialization {
private static ConfigContext instance;
public synchronized static ConfigContext getInstance() {
if (instance == null) {
instance = new ConfigContext();
}
return instance;
}
}
说明:由于synchronized会导致性能开销,如果上述同步方法被频繁调用,将导致程序性能下降。
- 双重检查锁降低同步的开销,实现延迟初始化:
public class DoubleCheckedLocking{
private static Instance instance;
public static Instance getInstance(){
if(instance == null){//代码1
synchronized (DoubleCheckedLocking.class){
if(instance = null){
instance= new Instance();//代码2
}
}
}
return instance;
}
}
问题根源
主要在于代码2处,可以分解成三个动作:
memory = allocate();//1:分配对象内存空间
ctorInstance(memory);//2. 初始化对象
instance = memory; //3. 设置instance指向刚分配的内存地址
以上三个步骤,可能会被重排序:
memory = allocate();//1:分配对象内存空间
instance = memory; //3. 设置instance指向刚分配的内存地址,此时对象还未被初始化
ctorInstance(memory);//2. 初始化对象
此时如果线程a执行到代码2,线程b支持代码1判断到instance不为null,将继续使用instance对象,其实instance并未被初始化所以引发异常。
我们找到问题根源,指令重排序,所以我们可以采用手段组织上述 2,3进行重排序,或者重排序对其他线程不可见;
解决方案
- 采用volatile解决
public class DoubleCheckedLocking{
private volatile static Instance instance;
public static Instance getInstance(){
if(instance == null){//代码1
synchronized (DoubleCheckedLocking.class){
if(instance = null){
instance= new Instance();//代码2
}
}
}
return instance;
}
}
- 基于类初始化方案
public class InstanceFactory{
private static class InstanceHolder{
public static Instance instance= new Instance();
}
public static Instance getInstance(){
return InstanceHolder.instance;//引发InstanceHolder类被初始化,初始化一个类包含执行这个类的静态初始化和初始化这个类中声明的静态字段
}
}
Java语言规范规定,发生下列任意一种情况时,一个类或接口类型T将被初始化。
- T是一个类,而且一个T类型的实例被创建
- T是一个类,且T中声明的一个静态方法被调用
- T中声明的一个静态字段被赋值
- T中声明的一个静态字段被使用,而且这个字段不是一个常量字段
同时Java语言规范规定,每个类或者接口C,都有一个唯一的初始化锁LC与之对应。