在双重检查锁中,首先检查实例是否已经被创建,如果没有被创建,则进行同步操作,然后再次检查实例是否已经被创建。这种方式可以避免多个线程进入外层if,同时创建实例的问题,从而提高性能和效率。
多线程环境下还会存在创建实例未初始化问题,参考如下代码
public class Singleton4 implements Serializable {
private Singleton4() {
System.out.println("private Singleton4()");
}
private static volatile Singleton4 INSTANCE = null; // 可见性,有序性
public static Singleton4 getInstance() {
if (INSTANCE == null) {
synchronized (Singleton4.class) {
if (INSTANCE == null) {//第二次检查INSTANCE的原因是第一次创建INSTANCE的时候,2个线程可能都进入第一个if条件
INSTANCE = new Singleton4();
}
}
}
return INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
了解下singleton = new Singleton()这段代码其实不是原子性的操作,它至少分为以下3个步骤:
-
给singleton对象分配内存空间
-
调用Singleton类的构造函数等,初始化singleton对象
-
将singleton对象指向分配的内存空间,这步一旦执行了,那singleton对象就不等于null了
这里还需要知道一点,就是有时候JVM会为了优化,而做指令重排序的操作,这里的指令,指的是CPU层面的。
正常情况下,singleton = new Singleton()的步骤是按照1->2->3这种步骤进行的,但是一旦JVM做了指令重排序,那么顺序很可能编程1->3->2;
如果是这种顺序,可以发现,在3步骤执行完singleton对象就不等于null,但是它其实还没做步骤二的初始化工作,但是另一个线程进来时发现,singleton不等于null了,就这样把半成品的实例返回去,调用是会报错的。
所以,DCL使用volatile关键字,是为了禁止指令重排序,避免返回还没完成初始化的singleton对象,导致调用报错,也保证了线程的安全。