首先是大家所熟知的饿汉式和懒汉式单例,我相信大家耳熟能详了,该篇文章不在过多赘述。
我今天想要分享的是单例模式的安全性问题。
单例模式是可以被反射和序列化破坏的,那么怎么解决这一问题呢,我们先卖个关子。
以下是懒汉单例的代码,我想记录一下为什么对象要被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()生成新的实例对象,那么此时和恶汉单例肯定不是一个对象了,单例被破坏。继续往下。
如图,到这里会判断是否有个readResolve的方法,并且反射调用该方法,因为我们定义的该方法返回了单例对象,所以反射调用后返回的是HungrySingleton中初始化好的单例对象,代替desc.newInstance()初始化的新的对象,这样反序列化最终还是同一个单例对象。
三、反射破坏案例
HungrySingleton hungrySingleton = HungrySingleton.getInstance();
Class<HungrySingleton> hungrySingletonClass = HungrySingleton.class;
// getDeclaredConstructor可以获取私有构造器,不包括从父类继承的
Constructor<HungrySingleton> constructor = hungrySingletonClass.getDeclaredConstructor();
constructor.setAccessible(true);
HungrySingleton hungrySingletonInstance = constructor.newInstance();
System.out.println(hungrySingleton);
System.out.println(hungrySingletonInstance);
System.out.println("hungrySingleton == hungrySingletonObject:" + (hungrySingleton == hungrySingletonInstance));
结果不是同一单例对象
四、最安全的单例实现:Enum
public enum EnumSingleton implements Serializable {
HOLDER;
// 持有单例对象属性
private Object instance;
EnumSingleton(){
instance = new Object();
}
public static EnumSingleton getInstance() {
return HOLDER;
}
}
接下来测试枚举类型实行的单例的安全性
如图。序列化结果是同一对象,反射报错。
只要枚举是同一个对象,那么枚举中持有的单例对象也必然是同一个,这种方式就是相当于利用枚举天然的不可被序列化和不可被反射的特性把单例对象保护了起来。
反射调用enum的无参构造报错是因为enum没有无参构造,只有一个有参构造函数
那么我们就反射它的有参构造,会抛出该异常:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
查看Constructor的newInstance方法可知,如果反射的是枚举类型直接抛出该 异常,不允许反射调用。