双重检测锁实现
public static Singleton3 getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }
此方法将同步的内容移动到if内部,只有第一次创建才会同步,提高了效率。但是,该方法会受到指令重排序的影响
指令重排序:指令重排序是为了优化指令,提高程序运行效率。指令重排序包括编译器重排序和运行时重排序。JVM规范规定,指令重排序可以在不影响单线程程序执行结果前提下进行。例如 instance = new Singleton() 可分解为如下伪代码:
memory = allocate(); //1:分配对象的内存空间 ctorInstance(memory); //2:初始化对象 instance = memory; //3:设置instance指向刚分配的内存地址
但是经过指令重排序后会变成这样
memory = allocate(); //1:分配对象的内存空间 instance = memory; //3:设置instance指向刚分配的内存地址 //注意,此时对象还没有被初始化! ctorInstance(memory); //2:初始化对象
这样就会出现问题,线程A执行了instance = memory(),这一步对线程B是可见,那么,线程B判断if(instance == null)时便会发现instance已经不为空了,便会返回instance,但是,由于instance只是指向了内存地址,并没有真正的初始化,那么线程B将会返回一个未能完全初始化的instance。
双重检测锁修订版
于是,修订版出现,使用两个同步块加一个局部变量来试图解决此问题,先用同步块初始化局部变量,完全初始化后赋值给instance,以此保证instance是完全初始化的。
public class Singleton3 { private static Singleton3 instance = null; private Singleton3() {}; public static Singleton3 getInstance() { if(instance == null) { Singleton3 sc; synchronized (Singleton3.class) { sc = instance; if(sc == null) { synchronized (Singleton3.class) { if(sc == null) { sc = new Singleton3(); } } instance = sc; } } } return instance; } }但是,这种方法依然存在问题,java规定了同步块里的内容必须在对象锁释放之前执行完毕(也就是一个线程必须执行完才能让另一个线程执行),但并没有规定同步块之外的代码,必须在同步块执行完之后执行,也就意味着, instance = sc 在运行过程中很可能跑到内层同步块中。这样,指令重排序的问题又再次出现,也就是说,sc指向内存地址后还没有初始化时就赋值给了instance,导致外部线程直接得到没有完全初始化的instance。
在JDK1.5之后,可以使用volatile变量禁止指令重排序,让DCL生效
public class Singleton3 { //使用volatile修饰该对象 private static volatile Singleton3 instance; private Singleton3() { } public static Singleton3 getInstance() { if (instance == null) { synchronized (Singleton3.class) { if (instance == null) { instance = new Singleton3(); } } } return instance; } }