传统单例模式双重检查锁存在的问题
单例模式1.0:
public class Singleton { private static Singleton sInstance; public static Singleton getInstance() { if (sInstance == null) { // 1 sInstance = new Singleton(); } return sInstance; } private Singleton() { } }
这种方式很辣鸡,因为多线程环境下不能保证单例。
单例模式2.0:
public class Singleton { private static volatile Singleton sInstance; public static synchronized Singleton getInstance() { if (sInstance == null) { sInstance = new Singleton(); } return sInstance; } private Singleton() { } }
这种方式也很辣鸡,因为多线程环境下每个线程执行getInstance()都要阻塞,效率很低。
单例模式3.0:
public class Singleton { private static Singleton sInstance; public static Singleton getInstance() { if (sInstance == null) { // 位置1 synchronized (Singleton.class) { if (sInstance == null) { sInstance = new Singleton(); // 位置2 } } } return sInstance; } private Singleton() {} }
这种方式使用双重检查锁,多线程环境下执行getInstance()时先判断单例对象是否已经初始化,如果已经初始化,就直接返回单例对象,如果未初始化,就在同步代码块中先进行初始化,然后返回,效率很高。
但是这种方式是一个错误的优化,问题的根源出在位置2
sInstance =new Singleton();这句话创建了一个对象,他可以分解成为如下3行代码:
memory = allocate(); // 1.分配对象的内存空间 ctorInstance(memory); // 2.初始化对象 sInstance = memory; // 3.设置sInstance指向刚分配的内存地址
上述伪代码中的2和3之间可能会发生重排序,重排序后的执行顺序如下
memory = allocate(); // 1.分配对象的内存空间 sInstance = memory; // 2.设置sInstance指向刚分配的内存地址,此时对象还没有被初始化 ctorInstance(memory); // 3.初始化对象
因为这种重排序并不影响Java规范中的规范:intra-thread sematics允许那些在单线程内不会改变单线程程序执行结果的重排序。
但是多线程并发时可能会出现以下情况
线程B访问到的是一个还未初始化的对象。
解决方案1:
public class Singleton { private static volatile Singleton sInstance; public static Singleton getInstance() { if (sInstance == null) { synchronized (Singleton.class) { if (sInstance == null) { sInstance = new Singleton(); } } } return sInstance; } private Singleton() {} }
将对象声明为volatitle后,前面的重排序在多线程环境中将会被禁止
解决方案2:
public class Singleton { private Singleton(){}; private static class Inner{ private static Singleton SINGLETION=new Singleton(); } public static Singleton getInstance(){ return Inner.SINGLETION; } }
静态内部类不会随着外部类的初始化而初始化,他是要单独去加载和初始化的,当第一次执行getInstance方法时,Inner类会被初始化。
静态对象SINGLETION的初始化在Inner类初始化阶段进行,类初始化阶段即虚拟机执行类构造器<clinit>()方法的过程。
虚拟机会保证一个类的<clinit>()方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的<clinit>()方法,其它线程都会阻塞等待。