双重检测锁实现
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;
}
}