一个单例还能写出花来吗?(二)
为了看到更详细的内容,我们执行 javap -c Singleton。
通过最终生成的字节码,我们其实发现本质上枚举的初始化通过static代码块来进行初始化。
考虑下类加载的几个步骤,加载->验证->准备->解析->初始化,最终初始化就是执行static代码块,而static代码块是绝对线程安全的,只能由JVM来调度,这样保证了线程安全。
枚举的实现方式好处还不止于此,除了一目了然的实现简单之外,还能防止其他几种实现方式避免不了的几个问题。
再说几种方式的问题
反射破坏单例
除了枚举之外,其他的几种方式都可以通过反射的方式达到破坏单例的目的,就随便以一个实现方式来举例,这里最终的输出结果是false。
如果拿去尝试反射创建枚举对象的话,则是会报错,可以自己动手尝试一下。
为什么会报错,可以直接看一下newInstance的源码,有一段特殊的关于枚举类型的判断,下图中我红色标记的部分。
序列化
除了众所周知的使用反射来破坏单例之外,还有另外一种能破坏单例的方式就是序列化。
对上面的饿汉方法实现序列化,然后得到的结果是false,序列化前后对象发生了改变。
其实关键的部分在于ois.readObject方法,一路跟踪最后找到一段代码如下:
所以很明显我们发现了最终实际上这里通过反射创建了一个新的对象,isInstantiable实际代表的应该是类或者属性是序列化的,那么久就返回true,我们这里肯定是true,所以最终产生了一个新的对象。
枚举为啥可以防止这个问题?枚举的实现方式不太一样而已,同样跟踪到枚举部分的实现逻辑。
下图中红框标注的部分就是枚举类型去实现反序列化的逻辑,最终只是通过valueOf方法查找枚举,不存在新建一个对象的逻辑。
那么,怎么防止其他方式序列化对单例的破坏?再往下看看源码,红框标注的意思只要有readResolve方法就可以解决问题了。
实际上,最终解决方案也很简单,单例类加上方法即可。
好了,打完收工。现在是北京时间4月15日凌晨1点整,困了,睡觉。
文章转自公众号:艾小仙