單例模式應該算是 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
分支。這裏,是對枚舉類型進行的處理。
感興趣的小夥伴能夠去研讀一下,最終的效果就是,咱們經過枚舉去定義單例,就能夠防止序列化破壞單例。
微信搜「煙雨星空」,白嫖更多好文~