public class Singleton {
private Singleton(){}
private static Singleton instance;
public static Singleton getInstance(){
//第一次判断,如果instance的值不为null,不需要抢占锁直接返回对象
if(instance==null){
synchronized (Singleton.class){
//第二次判断
if(instance==null){
instance=new Singleton();
}
}
}
return instance;
}
}
如上代码所示,如果第一次检查instance不为null,那么就不需要执行下面的同步代码块。因此,可以降低synchronized带来的性能开销。但是,在线程执行到第一次判断null时,instance引用的对象有可能还没有完全初始化。
我们可以对 instance=new Singleton() 进行分解为以下三行的伪代码:
memory=allocate();//1.分配对象的内存空间
ctorInstance(memory);//2.初始化对象
instance=memory;//3.设置instance指向刚分配的内存空间
在上面的代码里,有可能会被一些JIT编译器进行重排序。将2和3重排序后,变为以下的执行顺序:
memory=allocate();//1.分配对象的内存空间
instance=memory;//2.设置instance指向刚分配的内存空间(此时对象并未完成初始化)
ctorInstance(memory);//3.初始化对象
对重排序后的情况举例:
时间 | 线程A | 线程B |
---|---|---|
t1 | 分配对象内存空间 | |
t2 | 设置instance指向内存空间 | |
t3 | 判断instance不为null | |
t4 | 访问instance对象 | |
t5 | 初始化对象 | |
t6 | 访问instance对象 |
这时线程B访问的instance对象还没有初始化,此时会引起NullPointerException。
要实现线程安全的双重检查锁,只需将instance声明为volatile类型就可以了。
public class Singleton {
private Singleton(){}
//防止指令重排引起的空指针
private static volatile Singleton instance;
public static Singleton getInstance(){
//第一次判断,如果instance的值不为null,不需要抢占锁直接返回对象
if(instance==null){
synchronized (Singleton.class){
//第二次判断
if(instance==null){
instance=new Singleton();
}
}
}
return instance;
}
}
当声明的对象为volatile后,前面的伪代码将会被禁止重排序,并且会保证线程B在访问instance时一定是被初始化过的。