單例模式誰都會,破壞單例模式聽說過嗎?
美團到店的原題,手寫一個單例模式然后問如何破壞這個單例模式?
單例模式誰都會,懶漢、餓漢、雙重校驗鎖、匿名內(nèi)部類、Enum,倒背如流了都,那如何破壞單例呢?
以最簡單的餓漢式寫法為例:
所謂單例,就是保證一個類只有一個實例對象,那想要破壞單例模式,無非就是創(chuàng)建多個實例對象罷了
那單例模式的構(gòu)造函數(shù)都是 private 的,我們沒法直接通過 new 來構(gòu)造對象,也就是說通過 new 這種方式去破壞單例的可能性是不存在的,得另尋他路。
除了 new,創(chuàng)建對象的方式還有 clone,反序列化,以及反射。
要調(diào)用 clone 方法,那么必須實現(xiàn) Cloneable 接口,但是單例模式是不能實現(xiàn)這個接口的,因此排除這種可能性。所以我們要討論的其實就是如何通過反序列化和反射對單例模式進行破壞
反序列化破壞單例
序列化是破壞單例模式的一大利器。相比于克隆,實現(xiàn)序列化在實際操作中更加不可避免,有些類,它就是一定要序列化。
下面我們來做個測試,在上面的單例模式中實現(xiàn)序列化接口,然后先通過 getInstance 拿到一個對象,對這個對象進行序列化再反序列化拿到一個對象,比較兩個對象是否是同一個對象:
結(jié)果為 false,說明通過對 Singleton 的序列化再反序列化得到的對象是一個新的對象,這就破壞了 Singleton 的單例性。
反序列化是怎么創(chuàng)建一個新對象的?
我們可以點擊 readObject 這個方法看看
核心是 readObject0,繼續(xù)點進去:
根據(jù)傳入?yún)?shù)類型的不同,調(diào)用了不同的方法進行反序列化,點進針對 Object 的 readOrdinaryObject 方法看看:
真相大白了,反序列化底層其實就是使用了反射幫我們創(chuàng)建了一個新的對象。
如何阻止反序列化破壞單例?
現(xiàn)在我們在 Singleton 類中實現(xiàn)一個 readResolve 方法,該方法直接返回了這個單例對象:
重新執(zhí)行下,發(fā)現(xiàn)結(jié)果為 true!也就是說 instance1 和 instance2 是同一個對象!
具體是什么原理,我們來看看剛才的 readOrdinaryObject 方法:
可以看到,在條件判斷中 desc.hasReadResolveMethod() 會判斷類是否有 readResolve() 方法,如果有的話會通過desc.invokeReadResolve(obj) 去反射調(diào)用該方法,由于我們的 readResolve 方法直接返回了 instance,不會創(chuàng)建一個新對象,這樣最終就保證了類實例對象的唯一性
所以,如果想要防止單例被反序列化破壞,就讓單例類實現(xiàn) readResolve() 方法
反射破壞單例
上面說到,反序列化底層其實就是通過反射來創(chuàng)建一個新對象的,我們直接來看反射是怎么破壞單例的:
執(zhí)行結(jié)果當(dāng)然是 false 了
如何阻止反射破壞單例?
反射是怎么創(chuàng)建新對象的?是通過類的構(gòu)造函數(shù)來的
所以如果我們想要阻止反射破壞單例,我們就需要修改類的構(gòu)造函數(shù):
重新執(zhí)行一遍我們的代碼,不出所料拋異常了,這樣便防止了單例被反射破壞:
不過這種構(gòu)造函數(shù)判斷的方法,只能阻止餓漢式的單例模式,沒法阻止懶漢式的單例!
我們可以來寫個懶漢模式測試下:
執(zhí)行下,發(fā)現(xiàn)結(jié)果仍然是拋異常:
什么情況?
別急,我們把 instance1 和 instance2 的構(gòu)建順序調(diào)換下:
再執(zhí)行,結(jié)果就是 false 了?。?!
這是因為懶漢式的對象只有調(diào)用的時候才被創(chuàng)建,我們先調(diào)用反射通過私有構(gòu)造函數(shù)來創(chuàng)建對象,這樣就越過了 instance != null 的判斷,不會拋異常,再通過 getInstance 創(chuàng)建對象,這兩個對象就不是同一個對象了,即單例模式被破壞了。
總結(jié)下,如果今后需要自己手動實現(xiàn)一個單例的話,可以選擇【構(gòu)造函數(shù)判斷】+【實現(xiàn) readResolve() 方法】的方式 來防止單例被破壞。
優(yōu)雅的單例實現(xiàn):Enum
那如果我不想在構(gòu)造函數(shù)里面做判斷,也不想寫 readResolve() 方法,我就想安安靜靜寫個單例,有沒有更簡單更優(yōu)雅的方法?
答案是有的!可以選擇使用 Enum 枚舉來實現(xiàn)單例模式
用反射來測試下,結(jié)果是直接拋異常了 java.lang.NoSuchMethodException
簡單來說就是因為 singletonClass.getDeclaredConstructor() 沒有找到 Singleton 的無參構(gòu)造器,這是為啥?
主要是因為,一旦一個類聲明為枚舉,實際上就是繼承了 java.lang.Enum,來看看 Enum 類源碼:
Enum 有兩個參數(shù) name 和 ordial 兩個屬性,我們自己寫的單例類繼承了父類 Enum 的構(gòu)造函數(shù),所以在上述的 getDecalredConstructor 才會找不到無參構(gòu)造器,那么是不是我們?nèi)フ{(diào)用父類的構(gòu)造器就可以了呢?我們來測試一下:
哦吼,運行后直接拋 IllegalArgumentException 異常了
無法通過反射創(chuàng)建 Enum 對象!??!我們點進去報錯的 22 行即 constructor.newInstance 一探究竟:
簡單來說就是反射在通過 newInstance 創(chuàng)建對象時,會檢查該類是否被 ENUM 修飾,如果是則直接拋出異常,反射失敗,所以枚舉是不怕反射暴力破解構(gòu)造器的
上面說枚舉是可以阻止反射通過暴力破解構(gòu)造函數(shù)來破壞單例的,再來看枚舉是如何阻止反序列化破壞單例的。
事實上,枚舉對象的序列化、反序列化有自己的一套機制:
- 序列化的時候,僅僅是將枚舉對象的 name 屬性輸出到結(jié)果中
- 反序列化的時候則是通過java.lang.Enum 的 valueOf? 方法 來根據(jù) name 查找枚舉對象
來看看 Enum.valueOf 方法:
繼續(xù)看 getEnumConstantsShared() 源碼:
水落石出啦,仍然是通過反射做的,先獲取枚舉類的 values() 方法,再得到所有枚舉對象。
簡單總結(jié)下:
- 每個枚舉對象都有一個唯一的name 屬性。
- 序列化只是將name 屬性序列化。
- 在反序列化的時候,通過一個Map(key,value) 存儲 name 和與之對應(yīng)的對象之間的映射,然后通過 name 就可以直接獲得原來的 Enum 對象,而不會創(chuàng)建一個新對象!也就是說反序列化 Enum 類對象拿到的仍然是原來的對象,這樣就使得 Enum 類型實現(xiàn)了單例模式下的序列化安全。