手写一个单例模式然后问如何破坏这个单例模式
美团到店的原题,手写一个单例模式然后问如何破坏这个单例模式?
单例模式谁都会,懒汉、饿汉、双重校验锁、匿名内部类、Enum,倒背如流了都,那如何破坏单例呢?
以最简单的饿汉式写法为例:
所谓单例,就是保证一个类只有一个实例对象,那想要破坏单例模式,无非就是创建多个实例对象罢了
那单例模式的构造函数都是 private 的,我们没法直接通过 new 来构造对象,也就是说通过 new 这种方式去破坏单例的可能性是不存在的,得另寻他路。
除了 new,创建对象的方式还有 clone,反序列化,以及反射。
要调用 clone 方法,那么必须实现 Cloneable
接口,但是单例模式是不能实现这个接口的,因此排除这种可能性。所以我们要讨论的其实就是如何通过反序列化和反射对单例模式进行破坏
反序列化破坏单例
序列化是破坏单例模式的一大利器。相比于克隆,实现序列化在实际操作中更加不可避免,有些类,它就是一定要序列化。
下面我们来做个测试,在上面的单例模式中实现序列化接口,然后先通过 getInstance 拿到一个对象,对这个对象进行序列化再反序列化拿到一个对象,比较两个对象是否是同一个对象:
结果为 false,说明通过对 Singleton 的序列化再反序列化得到的对象是一个新的对象,这就破坏了 Singleton 的单例性。
反序列化是怎么创建一个新对象的?
我们可以点击 readObject
这个方法看看
核心是 readObject0
,继续点进去:
根据传入参数类型的不同,调用了不同的方法进行反序列化,点进针对 Object 的 readOrdinaryObject
方法看看:
真相大白了,反序列化底层其实就是使用了反射帮我们创建了一个新的对象。
如何阻止反序列化破坏单例?
现在我们在 Singleton
类中实现一个 readResolve
方法,该方法直接返回了这个单例对象:
重新执行下,发现结果为 true!也就是说 instance1 和 instance2 是同一个对象!
具体是什么原理,我们来看看刚才的 readOrdinaryObject
方法:
可以看到,在条件判断中 desc.hasReadResolveMethod()
会判断类是否有 readResolve()
方法,如果有的话会通过 desc.invokeReadResolve(obj)
去反射调用该方法,由于我们的 readResolve 方法直接返回了 instance,不会创建一个新对象,这样最终就保证了类实例对象的唯一性
所以,如果想要防止单例被反序列化破坏,就让单例类实现 readResolve()
方法
反射破坏单例
上面说到,反序列化底层其实就是通过反射来创建一个新对象的,我们直接来看反射是怎么破坏单例的:
执行结果当然是 false 了
如何阻止反射破坏单例?
反射是怎么创建新对象的?是通过类的构造函数来的
所以如果我们想要阻止反射破坏单例,我们就需要修改类的构造函数:
重新执行一遍我们的代码,不出所料抛异常了,这样便防止了单例被反射破坏:
不过这种构造函数判断的方法,只能阻止饿汉式的单例模式,没法阻止懒汉式的单例!
我们可以来写个懒汉模式测试下:
执行下,发现结果仍然是抛异常:
什么情况?
别急,我们把 instance1 和 instance2 的构建顺序调换下:
再执行,结果就是 false 了!!!
这是因为懒汉式的对象只有调用的时候才被创建,我们先调用反射通过私有构造函数来创建对象,这样就越过了 instance != null
的判断,不会抛异常,再通过 getInstance 创建对象,这两个对象就不是同一个对象了,即单例模式被破坏了。
总结下,如果今后需要自己手动实现一个单例的话,可以选择【构造函数判断】+【实现 readResolve() 方法】的方式
来防止单例被破坏
优雅的单例实现:Enum
那如果我不想在构造函数里面做判断,也不想写 readResolve()
方法,我就想安安静静写个单例,有没有更简单更优雅的方法?
答案是有的!可以选择使用 Enum 枚举来实现单例模式
用反射来测试下,结果是直接抛异常了 java.lang.NoSuchMethodException
简单来说就是因为 singletonClass.getDeclaredConstructor()
没有找到 Singleton 的无参构造器,这是为啥?
主要是因为,一旦一个类声明为枚举,实际上就是继承了 java.lang.Enum
类,来看看 Enum 类源码:
Enum 有两个参数 name
和 ordial
两个属性,我们自己写的单例类继承了父类 Enum 的构造函数,所以在上述的 getDecalredConstructor 才会找不到无参构造器,那么是不是我们去调用父类的构造器就可以了呢?我们来测试一下:
哦吼,运行后直接抛 IllegalArgumentException
异常了
无法通过反射创建 Enum 对象!!!我们点进去报错的 22 行即 constructor.newInstance
一探究竟:
简单来说就是反射在通过 newInstance
创建对象时,会检查该类是否被 ENUM
修饰,如果是则直接抛出异常,反射失败,所以枚举是不怕反射暴力破解构造器的
上面说枚举是可以阻止反射通过暴力破解构造函数来破坏单例的,再来看枚举是如何阻止反序列化破坏单例的。
事实上,枚举对象的序列化、反序列化有自己的一套机制:
- 序列化的时候,仅仅是将枚举对象的
name
属性输出到结果中 - 反序列化的时候则是通过 java.lang.Enum 的
valueOf
方法 来根据name
查找枚举对象
来看看 Enum.valueOf
方法:
继续看 getEnumConstantsShared()
源码:
水落石出啦,仍然是通过反射做的,先获取枚举类的 values()
方法,再得到所有枚举对象。
简单总结下:
- 每个枚举对象都有一个唯一的
name
属性 - 序列化只是将
name
属性序列化 - 在反序列化的时候,通过一个
Map(key,value)
存储name
和与之对应的对象之间的映射,然后通过name
就可以直接获得原来的 Enum 对象,而不会创建一个新对象!也就是说反序列化 Enum 类对象拿到的仍然是原来的对象,这样就使得 Enum 类型实现了单例模式下的序列化安全