前言
單例模式應(yīng)該算是 23 種設(shè)計(jì)模式中,最常見(jiàn)最容易考察的知識(shí)點(diǎn)了。經(jīng)常會(huì)有面試官讓手寫(xiě)單例模式,別到時(shí)候傻乎乎的說(shuō)我不會(huì)。
之前,我有介紹過(guò)單例模式的幾種常見(jiàn)寫(xiě)法。還不知道的,傳送門(mén)看這里:
本篇文章將展開(kāi)一些不太容易想到的問(wèn)題。帶著你思考一下,傳統(tǒng)的單例模式有哪些問(wèn)題,并給出解決方案。讓面試官眼中一亮,心道,小伙子有點(diǎn)東西??!
以下,以 DCL 單例模式為例。
DCL 單例模式
DCL 就是 Double Check Lock 的縮寫(xiě),即雙重檢查的同步鎖。代碼如下,
public class Singleton {
//注意,此變量需要用volatile修飾以防止指令重排序
private static volatile Singleton singleton = null;
private Singleton(){
}
public static Singleton getInstance(){
//進(jìn)入方法內(nèi),先判斷實(shí)例是否為空,以確定是否需要進(jìn)入同步代碼塊
if(singleton == null){
synchronized (Singleton.class){
//進(jìn)入同步代碼塊時(shí)再次判斷實(shí)例是否為空
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
乍看,以上的寫(xiě)法沒(méi)有什么問(wèn)題,而且我們確實(shí)也經(jīng)常這樣寫(xiě)。
但是,問(wèn)題來(lái)了。
DCL 單例一定能確保線程安全嗎?
有的小伙伴就會(huì)說(shuō),你這不是廢話么,大家不都這樣寫(xiě)么,肯定是線程安全的啊。
確實(shí),在正常情況,我可以保證調(diào)用 getInstance 方法兩次,拿到的是同一個(gè)對(duì)象。
但是,我們知道 Java 中有個(gè)很強(qiáng)大的功能——反射。對(duì)的,沒(méi)錯(cuò),就是他。
通過(guò)反射,我就可以破壞單例模式,從而調(diào)用它的構(gòu)造函數(shù),來(lái)創(chuàng)建不同的對(duì)象。
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();
//通過(guò)反射拿到無(wú)參構(gòu)造,設(shè)為可訪問(wèn)
ctr.setAccessible(true);
Singleton singleton2 = ctr.newInstance();
System.out.println(singleton2.hashCode()); // 895328852
}
}
我們會(huì)發(fā)現(xiàn),通過(guò)反射就可以直接調(diào)用無(wú)參構(gòu)造函數(shù)創(chuàng)建對(duì)象。我管你構(gòu)造器是不是私有的,反射之下沒(méi)有隱私。
打印出的 hashCode 不同,說(shuō)明了這是兩個(gè)不同的對(duì)象。

那怎么防止反射破壞單例呢?
很簡(jiǎn)單,既然你想通過(guò)無(wú)參構(gòu)造來(lái)創(chuàng)建對(duì)象,那我就在構(gòu)造函數(shù)里多判斷一次。如果單例對(duì)象已經(jīng)創(chuàng)建好了,我就直接拋出異常,不讓你創(chuàng)建就可以了。
修改構(gòu)造函數(shù)如下,

再次運(yùn)行測(cè)試代碼,就會(huì)拋出異常。

有效的阻止了通過(guò)反射去創(chuàng)建對(duì)象。
那么,這樣寫(xiě)單例就沒(méi)問(wèn)題了嗎?
這時(shí),機(jī)靈的小伙伴肯定就會(huì)說(shuō),既然問(wèn)了,那就是有問(wèn)題(可真是個(gè)小機(jī)靈鬼)。
但是,是有什么問(wèn)題呢?
我們知道,對(duì)象還可以進(jìn)行序列化反序列化。那如果我把單例對(duì)象序列化,再反序列化之后的對(duì)象,還是不是之前的單例對(duì)象呢?
實(shí)踐出真知,我們測(cè)試一下就知道了。
// 給 Singleton 添加序列化的標(biāo)志,表明可以序列化
public class Singleton implements Serializable{
... //省略不重要代碼
}
//測(cè)試是否返回同一個(gè)對(duì)象
public class TestDCL {
public static void main(String[] args) throws Exception {
Singleton singleton1 = Singleton.getInstance();
System.out.println(singleton1.hashCode()); // 723074861
//通過(guò)序列化對(duì)象,再反序列化得到新對(duì)象
String filePath = "D:\\singleton.txt";
saveToFile(singleton1,filePath);
Singleton singleton2 = getFromFile(filePath);
System.out.println(singleton2.hashCode()); // 1259475182
}
//將對(duì)象寫(xiě)入到文件
private static void saveToFile(Singleton singleton, String fileName){
try {
FileOutputStream fos = new FileOutputStream(fileName);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(singleton); //將對(duì)象寫(xiě)入oos
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//從文件中讀取對(duì)象
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;
}
}
可以發(fā)現(xiàn),我把單例對(duì)象序列化之后,再反序列化之后得到的對(duì)象,和之前已經(jīng)不是同一個(gè)對(duì)象了。因此,就破壞了單例。
那怎么解決這個(gè)問(wèn)題呢?
我先說(shuō)解決方案,一會(huì)兒解釋為什么這樣做可以。
很簡(jiǎn)單,在單例類中添加一個(gè)方法 readResolve 就可以了,方法體中讓它返回我們創(chuàng)建的單例對(duì)象。

然后再次運(yùn)行測(cè)試類會(huì)發(fā)現(xiàn),打印出來(lái)的 hashCode 碼一樣。
是不是很神奇。。。

readResolve 為什么可以解決序列化破壞單例的問(wèn)題?
我們通過(guò)查看源碼中一些關(guān)鍵的步驟,就可以解決心中的疑惑。
我們思考一下,序列化和反序列化的過(guò)程中,哪個(gè)流程最有可能有操作空間。
首先,序列化時(shí),就是把對(duì)象轉(zhuǎn)為二進(jìn)制存在 ``ObjectOutputStream` 流中。這里,貌似好像沒(méi)有什么特殊的地方。
其次,那就只能看反序列化了。反序列化時(shí),需要從 ObjectInputStream 對(duì)象中讀取對(duì)象,正常讀出來(lái)的對(duì)象是一個(gè)新的不同的對(duì)象,為什么這次就能讀出一個(gè)相同的對(duì)象呢,我猜這里會(huì)不會(huì)有什么貓膩?
應(yīng)該是有可能的。所以,來(lái)到我們寫(xiě)的方法 getFromFile中,找到這一行ois.readObject()。它就是從流中讀取對(duì)象的方法。

點(diǎn)進(jìn)去,查看 ObjectInputStream.readObject 方法,然后找到 readObject0()方法

再點(diǎn)進(jìn)去,我們發(fā)現(xiàn)有一個(gè) switch 判斷,找到 TC_OBJECT 分支。它是用來(lái)處理對(duì)象類型。

然后看到有一個(gè) readOrdinaryObject方法,點(diǎn)進(jìn)去。

然后找到這一行,isInstantiable() 方法,用來(lái)判斷對(duì)象是否可實(shí)例化。

由于 cons 構(gòu)造函數(shù)不為空,所以這個(gè)方法返回 true。因此構(gòu)造出來(lái)一個(gè) 非空的 obj 對(duì)象 。
再往下走,調(diào)用,hasReadResolveMethod 方法去判斷變量 readResolveMethod是否為非空。


我們?nèi)タ匆幌逻@個(gè)變量,在哪里有沒(méi)有賦值。會(huì)發(fā)現(xiàn)有這樣一段代碼,

點(diǎn)進(jìn)去這個(gè)方法 getInheritableMethod。發(fā)現(xiàn)它最后就是為了返回我們添加的readResolve 方法。

同時(shí)我們發(fā)現(xiàn),這個(gè)方法的修飾符可以是 public , protected 或者 private(我們當(dāng)前用的就是private)。但是,不允許使用 static 和 abstract 修飾。
再次回到 readOrdinaryObject方法,繼續(xù)往下走,會(huì)發(fā)現(xiàn)調(diào)用了 invokeReadResolve 方法。此方法,是通過(guò)反射調(diào)用 readResolve方法,得到了 rep 對(duì)象。


然后,判斷 rep 是否和 obj 相等 。 obj 是剛才我們通過(guò)構(gòu)造函數(shù)創(chuàng)建出來(lái)的新對(duì)象,而由于我們重寫(xiě)了 readResolve 方法,直接返回了單例對(duì)象,因此 rep 就是原來(lái)的單例對(duì)象,和 obj 不相等。
于是,把 rep 賦值給 obj ,然后返回 obj。
所以,最終得到這個(gè) obj 對(duì)象,就是我們?cè)瓉?lái)的單例對(duì)象。
至此,我們就明白了是怎么一回事。
一句話總結(jié)就是:當(dāng)從對(duì)象流 ObjectInputStream 中讀取對(duì)象時(shí),會(huì)檢查對(duì)象的類否定義了 readResolve 方法。如果定義了,則調(diào)用它返回我們想指定的對(duì)象(這里就指定了返回單例對(duì)象)。
總結(jié)
因此,完整的 DCL 就可以這樣寫(xiě),
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(){
//進(jìn)入方法內(nèi),先判斷實(shí)例是否為空,以確定是否需要進(jìn)入同步代碼塊
if(singleton == null){
synchronized (Singleton.class){
//進(jìn)入同步代碼塊時(shí)再次判斷實(shí)例是否為空
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
// 定義readResolve方法,防止反序列化返回不同的對(duì)象
private Object readResolve(){
return singleton;
}
}
另外,不知道細(xì)心的讀者有沒(méi)有發(fā)現(xiàn),在看源碼中 switch 分支有一個(gè) case TC_ENUM 分支。這里,是對(duì)枚舉類型進(jìn)行的處理。
感興趣的小伙伴可以去研讀一下,最終的效果就是,我們通過(guò)枚舉去定義單例,就可以防止序列化破壞單例。
微信搜「煙雨星空」,白嫖更多好文~