單例模式誰都會,破壞單例模式聽說過嗎?
美團到店的原題,手寫一個單例模式然后問如何破壞這個單例模式?
單例模式誰都會,懶漢、餓漢、雙重校驗鎖、匿名內部類、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 類型實現了單例模式下的序列化安全。