我们直接进入正题
这是双重校验锁的单例模式
private static Singleton instance; //1
public static Singleton getSingleton(){
if (instance == null){ //2
synchronized (Singleton.class){ //3
if (instance == null) //4
instance = new Singleton(); //5 //实例化
}
}
return instance;
}
这段代码看起来两全其美
①多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象
②在对象创建完之后,执行getSingleton方法不需要获取锁,直接返回已创建好的对象
双重校验锁看起来似乎很完美,但这是一个错误的优化,在代码执行到第二行时,代码读取instance不为null,instance引用的对象有可能还没有完成初始化。
一、问题的根源
在创建一个对象时,可分这三个步骤
①分配对象的内存空间
②初始化对象
③将引用变量指向分配的内存地址(在这一步,引用变量就已经不是null了)
但是呢,上面步骤的2,3有可能被重排序,指向顺序如下
①分配对象的内存空间
②将引用变量指向分配的内存地址(在这一步,引用变量就已经不是null了)//此时,对象还没有初始化
②初始化对象
所以当第一个线程执行到第二步时,第二个线程第一次校验发现instance不是空,在使用时就出报错。
抠张图看看
所以我们想办法
①不允许2、3步重排序
②运行2、3步重排序,但不允许其他线程看到这个重排序
解决方法一:基于vloatile的解决方案
我们只需要一点小小的修改,就可以实现线程安全的延迟初始化,那就将instance用volatile修饰,也就是下面的代码
private static volatile Singleton instance;
public static Singleton getSingleton(){
if (instance == null){
synchronized (Singleton.class){
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
当对象声明为volatile时,步骤2和步骤3之前的重排序,在多线程环境中将会被禁止。
解决方法二:基于类初始化的解决方案
JVM在类的初始化阶段,会执行类的初始化,在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。
基于这个特性,可以实现另一种线程安全的延迟初始化方案
private static class SingletonFactory{
public static Singleton singleton = new Singleton();
}
private static Singleton getSingleton(){
return SingletonFactory.singleton;
}
抠张图看看这是为什么吧
Java语言规范规定,对于每一个类或者接口,都有一个唯一的初始化锁预支对应。JVM在类初始化期间会获取这个锁,并且每隔线程至少获取一次锁来确保这个类已经被初始化过了。