Java单例模式与反射及序列化
单例模式的注意点
单例模式与反射
单例模式最根本的在于类只能有一个实例,如果通过反射来构建这个类的实例,单例模式就会被破坏,下面我们通过例子来看下:
/**
* 静态内部类式单例模式
*/
class Singleton implements Serializable{
private static class SingletonClassInstance {
private static final Singleton instance = new Singleton();
}
//方法没有同步,调用效率高
public static Singleton getInstance() {
return SingletonClassInstance.instance;
}
private Singleton() {}
}
相信大家对于这个单例的这种实现方式肯定不陌生,下面我们来看看通过反射来创建类实例会不会破坏单例模式。main函数代码如下:
Singleton sc1 = Singleton.getInstance();
Singleton sc2 = Singleton.getInstance();
System.out.println(sc1); // sc1,sc2是同一个对象
System.out.println(sc2);
/*通过反射的方式直接调用私有构造器(通过在构造器里抛出异常可以解决此漏洞)*/
Class<Singleton> clazz = (Class<Singleton>) Class.forName("com.learn.example.Singleton");
Constructor<Singleton> c = clazz.getDeclaredConstructor(null);
c.setAccessible(true); // 跳过权限检查
Singleton sc3 = c.newInstance();
Singleton sc4 = c.newInstance();
System.out.println("通过反射的方式获取的对象sc3:" + sc3); // sc3,sc4不是同一个对象
System.out.println("通过反射的方式获取的对象sc4:" + sc4);
下面我们来看输出:
com.learn.example.Singleton@52e922
com.learn.example.Singleton@52e922
通过反射的方式获取的对象sc3:com.learn.example.Singleton@25154f
通过反射的方式获取的对象sc4:com.learn.example.Singleton@10dea4e
我们看到正常的调用getInstance是符合我们预期的,如果通过反射(绕过检查,通过反射可以调用私有的),那么单例模式其实是失效了,我们创建了两个完全不同的对象sc3和sc4。我们如何来修复这个问题呢?反射需要调用构造函数,那我们可以在构造函数里面进行判断。修复代码如下:
class Singleton implements Serializable{
private static class SingletonClassInstance {
private static final Singleton instance = new Singleton();
}
//方法没有同步,调用效率高
public static Singleton getInstance() {
return SingletonClassInstance.instance;
}
//防止反射获取多个对象的漏洞
private Singleton() {
if (null != SingletonClassInstance.instance)
throw new RuntimeException();
}
}
我们看到唯一的改进在于,构造函数里面添加了判断,如果当前已有实例,通过抛出异常来阻止反射创建对象。我们来看下输出:
com.learn.example.Singleton@52e922
com.learn.example.Singleton@52e922
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
at java.lang.reflect.Constructor.newInstance(Unknown Source)
at com.learn.example.RunMain.main(RunMain.java:45)
Caused by: java.lang.RuntimeException
at com.learn.example.Singleton.<init>(RunMain.java:28)
... 5 more
我们看到,我们通过反射创建对象的时候会抛出异常了。
单例模式与序列化
除了反射以外,反序列化过程也会破坏单例模式,我们来看下现阶段反序列化输出的结果:
com.learn.example.Singleton@52e922
com.learn.example.Singleton@52e922
对象定义了readResolve()方法,通过反序列化得到的对象:com.learn.example.Singleton@16ec8df
我们看到反序列化后的对象和原对象sc1已经不是同一个对象了。我们需要对反序列化过程进行处理,处理代码如下:
//防止反序列化获取多个对象的漏洞。
//无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。
//实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象
private Object readResolve() throws ObjectStreamException {
return SingletonClassInstance.instance;
}
我们从注释里面也可以看出来,readResolve方法会将原来反序列化出来的对象进行覆盖。我们丢弃原来反序列化出来的对象,使用已经创建的好的单例对象进行覆盖。我们来看现在的输出:
com.learn.example.Singleton@52e922
com.learn.example.Singleton@52e922
对象定义了readResolve()方法,通过反序列化得到的对象:com.learn.example.Singleton@52e922
关于readResolve这个方法的详细解释可以看这篇文章: 序列化的相关方法介绍
使用枚举实现单例
Effective Java中推荐使用枚举来实现单例,因为枚举实现单例可以阻止反射及序列化的漏洞,下面我们通过例子来看下:
class Resource{}
/**
* 使用枚举实现单例
*/
enum SingletonEnum{
INSTANCE;
private Resource instance;
SingletonEnum() {
instance = new Resource();
}
public Resource getInstance() {
return instance;
}
}
我们在main方法中调用代码:
Resource resource1 = SingletonEnum.INSTANCE.getInstance();
Resource resource2 = SingletonEnum.INSTANCE.getInstance();
System.out.println(resource1);
System.out.println(resource2);
输出如下:
com.learn.example.Resource@52e922
com.learn.example.Resource@52e922
我们看到,通过枚举我们实现了单例,那么枚举是如何保证单例的(如何满足多线程及序列化的标准的)?其实枚举是一个普通的类,它继承自java.lang.Enum类。我们将上面的class文件反编译后,会得到如下代码:
public final class SingletonEnum extends Enum<SingletonEnum> {
public static final SingletonEnum INSTANCE;
public static SingletonEnum[] values();
public static SingletonEnum valueOf(String s);
static {};
}
由反编译后的代码可知,INSTANCE 被声明为static 的,在类加载过程,可以知道虚拟机会保证一个类的() 方法在多线程环境中被正确的加锁、同步。所以,枚举实现是在实例化时是线程安全。
枚举实现与序列化
Java规范中规定,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。 在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象。 也就是说,以下面枚举为例,序列化的时候只将 INSTANCE 这个名称输出,反序列化的时候再通过这个名称,查找对于的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。 Effective Java中单元素的枚举类型被作者认为是实现Singleton的最佳方法。
转载自:https://juejin.cn/post/6844903679602982919