单例模式应该算是 23 种设计模式中,最多见最容易考察的知识点了。常常会有面试官让手写单例模式,别到时候傻乎乎的说我不会。java
以前,我有介绍过单例模式的几种常见写法。还不知道的,传送门看这里:面试
设计模式之单例模式设计模式
本篇文章将展开一些不太容易想到的问题。带着你思考一下,传统的单例模式有哪些问题,并给出解决方案。让面试官眼中一亮,心道,小伙子有点东西啊!安全
如下,以 DCL 单例模式为例。微信
DCL 就是 Double Check Lock 的缩写,即双重检查的同步锁。代码以下,函数
public class Singleton { //注意,此变量须要用volatile修饰以防止指令重排序 private static volatile Singleton singleton = null; private Singleton(){ } public static Singleton getInstance(){ //进入方法内,先判断实例是否为空,以肯定是否须要进入同步代码块 if(singleton == null){ synchronized (Singleton.class){ //进入同步代码块时再次判断实例是否为空 if(singleton == null){ singleton = new Singleton(); } } } return singleton; } }
乍看,以上的写法没有什么问题,并且咱们确实也常常这样写。测试
可是,问题来了。this
有的小伙伴就会说,你这不是废话么,你们不都这样写么,确定是线程安全的啊。线程
确实,在正常状况,我能够保证调用 getInstance
方法两次,拿到的是同一个对象。设计
可是,咱们知道 Java 中有个很强大的功能——反射。对的,没错,就是他。
经过反射,我就能够破坏单例模式,从而调用它的构造函数,来建立不一样的对象。
public class TestDCL { public static void main(String[] args) throws Exception { Singleton singleton1 = Singleton.getInstance(); System.out.println(singleton1.hashCode()); // 723074861 Class<Singleton> clazz = Singleton.class; Constructor<Singleton> ctr = clazz.getDeclaredConstructor(); //经过反射拿到无参构造,设为可访问 ctr.setAccessible(true); Singleton singleton2 = ctr.newInstance(); System.out.println(singleton2.hashCode()); // 895328852 } }
咱们会发现,经过反射就能够直接调用无参构造函数建立对象。我管你构造器是否是私有的,反射之下没有隐私。
打印出的 hashCode 不一样,说明了这是两个不一样的对象。
很简单,既然你想经过无参构造来建立对象,那我就在构造函数里多判断一次。若是单例对象已经建立好了,我就直接抛出异常,不让你建立就能够了。
修改构造函数以下,
再次运行测试代码,就会抛出异常。
有效的阻止了经过反射去建立对象。
这时,机灵的小伙伴确定就会说,既然问了,那就是有问题(可真是个小机灵鬼)。
可是,是有什么问题呢?
咱们知道,对象还能够进行序列化反序列化。那若是我把单例对象序列化,再反序列化以后的对象,仍是不是以前的单例对象呢?
实践出真知,咱们测试一下就知道了。
// 给 Singleton 添加序列化的标志,代表能够序列化 public class Singleton implements Serializable{ ... //省略不重要代码 } //测试是否返回同一个对象 public class TestDCL { public static void main(String[] args) throws Exception { Singleton singleton1 = Singleton.getInstance(); System.out.println(singleton1.hashCode()); // 723074861 //经过序列化对象,再反序列化获得新对象 String filePath = "D:\\singleton.txt"; saveToFile(singleton1,filePath); Singleton singleton2 = getFromFile(filePath); System.out.println(singleton2.hashCode()); // 1259475182 } //将对象写入到文件 private static void saveToFile(Singleton singleton, String fileName){ try { FileOutputStream fos = new FileOutputStream(fileName); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(singleton); //将对象写入oos oos.close(); } catch (IOException e) { e.printStackTrace(); } } //从文件中读取对象 private static Singleton getFromFile(String fileName){ try { FileInputStream fis = new FileInputStream(fileName); ObjectInputStream ois = new ObjectInputStream(fis); return (Singleton) ois.readObject(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } return null; } }
能够发现,我把单例对象序列化以后,再反序列化以后获得的对象,和以前已经不是同一个对象了。所以,就破坏了单例。
我先说解决方案,一下子解释为何这样作能够。
很简单,在单例类中添加一个方法 readResolve 就能够了,方法体中让它返回咱们建立的单例对象。
而后再次运行测试类会发现,打印出来的 hashCode 码同样。
是否是很神奇。。。
咱们经过查看源码中一些关键的步骤,就能够解决心中的疑惑。
咱们思考一下,序列化和反序列化的过程当中,哪一个流程最有可能有操做空间。
首先,序列化时,就是把对象转为二进制存在 ``ObjectOutputStream` 流中。这里,貌似好像没有什么特殊的地方。
其次,那就只能看反序列化了。反序列化时,须要从 ObjectInputStream
对象中读取对象,正常读出来的对象是一个新的不一样的对象,为何此次就能读出一个相同的对象呢,我猜这里会不会有什么猫腻?
应该是有可能的。因此,来到咱们写的方法 getFromFile
中,找到这一行ois.readObject()
。它就是从流中读取对象的方法。
点进去,查看 ObjectInputStream.readObject 方法
,而后找到 readObject0()方法
再点进去,咱们发现有一个 switch 判断,找到 TC_OBJECT 分支。它是用来处理对象类型。
而后看到有一个 readOrdinaryObject方法
,点进去。
而后找到这一行,isInstantiable()
方法,用来判断对象是否可实例化。
因为 cons 构造函数不为空,因此这个方法返回 true。所以构造出来一个 非空的 obj 对象 。
再往下走,调用,hasReadResolveMethod
方法去判断变量 readResolveMethod
是否为非空。
咱们去看一下这个变量,在哪里有没有赋值。会发现有这样一段代码,
点进去这个方法 getInheritableMethod
。发现它最后就是为了返回咱们添加的readResolve
方法。
同时咱们发现,这个方法的修饰符能够是 public , protected 或者 private(咱们当前用的就是private)。可是,不容许使用 static 和 abstract 修饰。
再次回到 readOrdinaryObject
方法,继续往下走,会发现调用了 invokeReadResolve
方法。此方法,是经过反射调用 readResolve
方法,获得了 rep 对象。
而后,判断 rep 是否和 obj 相等 。 obj 是刚才咱们经过构造函数建立出来的新对象,而因为咱们重写了 readResolve 方法,直接返回了单例对象,所以 rep 就是原来的单例对象,和 obj 不相等。
因而,把 rep 赋值给 obj ,而后返回 obj。
因此,最终获得这个 obj 对象,就是咱们原来的单例对象。
至此,咱们就明白了是怎么一回事。
一句话总结就是:当从对象流 ObjectInputStream 中读取对象时,会检查对象的类否认义了 readResolve 方法。若是定义了,则调用它返回咱们想指定的对象(这里就指定了返回单例对象)。
所以,完整的 DCL 就能够这样写,
public class Singleton implements Serializable { //注意,此变量须要用volatile修饰以防止指令重排序 private static volatile Singleton singleton = null; private Singleton(){ if(singleton != null){ throw new RuntimeException("Can not do this"); } } public static Singleton getInstance(){ //进入方法内,先判断实例是否为空,以肯定是否须要进入同步代码块 if(singleton == null){ synchronized (Singleton.class){ //进入同步代码块时再次判断实例是否为空 if(singleton == null){ singleton = new Singleton(); } } } return singleton; } // 定义readResolve方法,防止反序列化返回不一样的对象 private Object readResolve(){ return singleton; } }
另外,不知道细心的读者有没有发现,在看源码中 switch 分支有一个 case TC_ENUM
分支。这里,是对枚举类型进行的处理。
感兴趣的小伙伴能够去研读一下,最终的效果就是,咱们经过枚举去定义单例,就能够防止序列化破坏单例。
微信搜「烟雨星空」,白嫖更多好文~