问题
下面是一段双重检查锁实现单例模式的代码
public
咋一看是没什么问题的,先私有化构造方法,不让外界调用,然后通过double-checked locking来初始化INSTANCE。
大部分情况下,这么写的确不会有问题,但是少部分线程竞争特别激烈的情况,这么写会返回空。
为什么
为什么会报空指针,这是因为初始化INSTANCE = new Singleton()这行代码,不是原子操作,而这内部可能存在指令重排序
java中new一个对象并赋值给静态变量,实际上存在着以下几步操作:
- 在堆内存中创建新的对象
- 将一份对象的引用用于调用类的构造方法进行初始化
- 将一份对象的引用与静态变量相关联
上面2,3步是完全独立的,不管哪条语句先执行都对结果没影响,因此如果构造方法逻辑复杂,相对耗时的时候,jvm会进行一个指令重排序,使3语句在2语句的前面执行,而恰巧这时候有多个线程访问,这个时候情况是这样的:
- 线程1执行外层if,此时instance为空,进入判断
- 线程1获取锁
- 在堆内存中创建新的对象
- 线程1将堆内存的对象与instance进行关联,关联后instance不再是null,但此时由于上面中的2语句未执行,instance未被初始化
- 线程2进入外层if
- 由于第四步,instance不再是null,但又由于instance未被初始化,所以最终返回的是一个未被初始化的instance对象,线程2return null;
- 线程1继续执行
- ....
继续探讨
为什么说INSTANCE = new Singleton()不是原子操作,我们可以反编译下class,看生成的字节码:
17:
17行new,新建一个Singleton对象,并拿到它的引用压入操作数栈中
20行dup,复制对象的引用压入操作数栈中
21行invokespecial,从操作数栈用弹出一个引用,用于调用类的构造方法进行初始化
24行putstatic,从操作数栈用弹出一个引用用于给成员变量赋值
其中21,24行存在指令重拍序的可能
如何解决
既然是指令重排序导致这个问题,那么就只要防止指令重排序就行了,我们知道java中的volatile关键字可以防止指令重排的问题,那么只需要对Instance使用volatile关键字修饰即可
private
更优解
volatile虽然可以防止指令重排序的问题,但这也导致了每次线程都是从主内存中获取INSTANCE的值,也会存在一定的性能上的浪费,如何解决这种问题呢?可以用单例的另一种实现方式,利用类加载的线程安全性,使用内部类来初始化instance,见代码:
public