之前说了单例模式,本文深入研究下单例的破坏,分别从序列化和反射两个角度对单例破坏进行说明。
## 序列化破坏单例
前面说过好几种单例模式,现在以饿汉模式来说明,序列化是如果破坏单例的。首先把之前的HungrySingleton实现Serializable接口,然后我们通过以下代码调用HungrySingleton:
```java
HungrySingleton instance = HungrySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
HungrySingleton newInstance = (HungrySingleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
```
结果:
> 单例模式.HungrySingleton@12a3a380</br>
单例模式.HungrySingleton@29453f44</br>
false
以上代码我们首先创建一个HungrySingleton,然后保存在文件中,之后再读出这个文件获得HungrySingleton,从上面的打印结果中,我们可以看到这是两个不同的对象了,要解决这个问题,只要在HungrySingleton中加入readResolve方法:
```java
public class HungrySingleton implements Serializable{
private final static HungrySingleton hungrySingleton;
static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
//加入该方法防止序列化破坏单例
private Object readResolve(){
return hungrySingleton;
}
}
```
加入readResolve方法后在运行代码,结果为:
> 单例模式.HungrySingleton@12a3a380</br>
单例模式.HungrySingleton@12a3a380</br>
true
## 反射破坏单例
除了序列化可以破坏单例,反射也可以破坏单例,我们还是以饿汉模式为例,用如下代码调用单例类:
```java
Class objectClass = HungrySingleton.class;
Constructor constructor = objectClass.getDeclaredConstructor();
constructor.setAccessible(true);
HungrySingleton instance = HungrySingleton.getInstance();
HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance==newInstance);
```
结果为:
> 单例模式.HungrySingleton@71dac704</br>
单例模式.HungrySingleton@123772c4</br>
false
可以看到,我们比较两个分别是正常获得的单例,和通过反射得到的单例,结果是两个不同的对象,这点提醒我们需要在构造方法中加入防御代码,代码如下:
```java
public class HungrySingleton implements Serializable{
private final static HungrySingleton hungrySingleton;
static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
//防御反射破坏单例
if(hungrySingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
//加入该方法防止序列化破坏单例
private Object readResolve(){
return hungrySingleton;
}
}
```
我们在构造方法中加入了防御代码,现在在执行之前的代码就会抛出异常了。同理,我们在静态内部类单例模式中也加入防御代码:
```java
public class StaticInnerClassSingleton {
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
private StaticInnerClassSingleton(){
if(InnerClass.staticInnerClassSingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
}
```
这样饿汉模式和静态内部类模式的单例都通过加入了防御代码可以防止反射破坏单例了,但是懒汉模式和DoubleCheck不行,为什么呢?我们考虑下,饿汉模式和静态内部类,都是在类加载的时候就创建实例的,换句话说,创建单例的实例都是在第一次调用getInstance获取单例之前,所以无论是正常调用还是通过反射调用,在调用构造器之前实例已经是创建好了,所以可以通过在构造器里判断实例是否为null来抛出异常。
而饿汉模式和DoubleCheck是延迟加载的,如果通过反射调用在通过getInstance方法之前,构造器里的判断实例就是null,也是无法防御住反射破坏单例了,所以之前文章介绍了5种单例模式中,饿汉模式和DoubleCheck是有安全漏洞的,剩下的3中,懒汉模式虽然可以防御住反射攻击,但是有性能缺陷。而枚举单例一方面用起来比较少,不如静态内部类直观,另一方面在也没有延迟加载,所以一般来说我还是用静态内部类单例模式多一些。
> 还有克隆破坏单例请看原型模式
单例模式(续) - 单例的破坏