我今天想要分享的是单例模式的安全性问题。
首先单例模式是可以被反射和序列化破坏的
第一、说明为什么会被破坏
第二、讲解如何避免这些问题
第三、什么是最安全的单例模式,以及为什么最安全
以下是懒汉单例的代码,我想记录一下为什么对象要被volatile修饰。
package com.liyl.study.design;
public class LazySingleton {
private static volatile LazySingleton lazySingleton = null;
private LazySingleton() { }
public static LazySingleton getInstance() {
if(lazySingleton == null) {
synchronized(LazySingleton.class) {
if(lazySingleton == null) {
/**
* 对象初始化分3个指令
* 1、分配内存给对象
* 2、初始化对象
* 3、将对象的引用指向lazySingleton,只要执行了这一步lazySingleton就不为null
*/
lazySingleton = new LazySingleton();
}
}
}
return lazySingleton;
}
}
正如我之前写过的一篇JMM的文章,提到了指令乱序执行优化有指令重排。
那么一个对象的赋值初始化是分3个指令的,并不是原子的。
1、分配内存给对象
2、初始化对象
3、将对象的引用指向lazySingleton,只要执行了这一步lazySingleton就不为null
那么第2和3条指令重排序并不影响结果,所以是可能会被打乱执行的。
如下图,假如线程0先执行了第3条指令,还没执行第2条指令,即对象并未真正初始化,但此时如果有线程1走到了 if(lazySingleton == null) 判断,因为线程0已经执行了第3指令,那么lazySingleton 就已经不为bull,不会进入if语句进行初始化过程,返回一个还未初始化的lazySingleton对象。那么一个未初始化的对象是一定不能使用的。
使用volatile修饰的对象,可以禁止重排序1、2、3就不会被重排序,lazySingleton也就可以正常初始化。
二、序列化破坏案例
如下,恶汉单例序列化和反序列化后生成了新的对象,这个怎么应对呢。
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
HungrySingleton hungrySingleton = HungrySingleton.getInstance();
ObjectOutputStream oop = new ObjectOutputStream(new FileOutputStream("d://singleton"));
oop.writeObject(hungrySingleton);
File file = new File("d://singleton");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
HungrySingleton hungrySingletonObject = (HungrySingleton) ois.readObject();
System.out.println(hungrySingleton);
System.out.println(hungrySingletonObject);
System.out.println("hungrySingleton == hungrySingletonObject:" + (hungrySingleton == hungrySingletonObject));
}
}
解决方式:
如下图,在恶汉单例的类中定义一个readResolve方法,返回该实例。
那么为什么要这么做呢。让我们去看看序列化方法ObjectInputStream的readObject方法的源码。
第一:
第二:进入方法后找到类型判断的switch,我们序列化的对象当然是Object子类,所以会进入readOrdinaryObject方法
三:如下图,但类默认实现了Serializable接口,desc.isInstantiable()会返回true,从而调用desc.newInstance()生成新的实例对象,那么序列化生成的对象和我们new的对象实例自然不是一个了,单例被破坏。
四:如图,到这里会判断我们的类中是否有个readResolve的方法,并且通过反射调用该方法,因为我们定义的该方法返回了单例对象本身,所以反射调用后返回的是HungrySingleton中初始化好的单例对象,代替desc.newInstance()初始化的新的对象,这样反序列化最终还是同一个单例对象。
四、最安全的单例实现:Enum
public enum EnumSingleton implements Serializable {
HOLDER;
// 持有单例对象属性
private Object instance;
EnumSingleton(){
instance = new Object();
}
public static EnumSingleton getInstance() {
return HOLDER;
}
}
最安全的单例模式还是通过枚举类返回,为什么呢?还是来看源码。
通过反射调用产生实例的方法会报错,是因为enum没有无参构造,只有一个有参构造函数,如下图
那么我们就反射调用它的有参构造,会抛出该异常:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
查看Constructor的newInstance方法可知,如果反射的是枚举类型直接抛出该异常,不允许反射调用。直接给抛出异常。如下图