眾所周知,在代碼中采用合理的設(shè)計模式酵幕,不僅僅能使代碼更容易被他人理解扰藕,同時也能使整體模塊擁有更合理的結(jié)構(gòu),方便后期擴展維護芳撒。因此就產(chǎn)生了一些“套路”邓深,而這些“套路”我們便稱之為“設(shè)計模式”。
另外笔刹,如果想要弄明白一些知識芥备,一定要分清楚順序,即遇到了什么問題
舌菜、要怎么解決
以及有沒有更好的辦法
萌壳,這樣帶著問題去思考,可以達到事半功倍的效果日月。
言歸正傳袱瓮,開始說單例模式。按照上面的思考順序爱咬,我們一步一步來分析尺借。
1. 有本參奏,無本退朝
開始上早朝了啊~平常我們在使用某個類的實例時精拟,直接使用關(guān)鍵字new
燎斩,便可創(chuàng)建一個實例對象虱歪。但有時候可能會頻繁使用某個實例對象,或者創(chuàng)建這個對象比較耗費資源瘫里,例如請了一個管家实蔽,需要管家?guī)湍愀梢恍┦拢偛荒苊看涡枰芗业臅r候就重新聘請一個吧谨读?最好的方法就是長期聘請這個管家局装,需要的時候直接吩咐就行了。突然發(fā)現(xiàn)我這個例子舉得是很恰當袄椭场铐尚!
通過上面的闡述,我們遇到一個問題哆姻,那就是某個類的實例對象頻繁使用宣增,或者創(chuàng)建時比較費時費事時,希望只創(chuàng)建一次對象矛缨,并且一個就夠了(你要是非得請兩個管家爹脾,我只能說你有錢)。在這種情況下箕昭,我們來開始思考如果解決這個問題灵妨。
2. 建言獻策,百花齊放
大家都開始獻上良策啊落竹,一個一個來泌霍,第一位,趙學士你先發(fā)言~
2.1 餓漢式
其實挺好解決的述召,看我下面的代碼:
// 趙學士的方案
public class Singleton {
private static Singleton sInstance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return sInstance;
}
}
構(gòu)造方法私有化這就不解釋了朱转,保證外部不能隨便通過new
關(guān)鍵字來創(chuàng)建對象;靜態(tài)成員變量sIntance
在Singleton
這個類加載的時候就初始化积暖,創(chuàng)建了Singleton
對象藤为,并且只存在一個;通過Singleton.getInstance()
方法可以獲取該實例對象夺刑,這就是單例模式凉蜂!這就解決了問題啊同志們!
不過錢大臣想了想說性誉,不過這種方法好像有點弊端,假如我現(xiàn)在還不需要管家茎杂,總不能讓我白花錢養(yǎng)著吧错览?能不能在我需要的時候再花錢聘請管家?
誒~~你這么一說也有道理啊煌往,那錢大臣倾哺,說說你的辦法轧邪。
2.2 懶漢式
話不多說,先看代碼:
// 錢大臣的方案
public class Singleton {
private static Singleton sInstance;
private Singleton() {
}
public static Singleton getInstance() {
if (sInstance == null) {
sInstance = new Singleton();
}
return sInstance;
}
}
怎么樣羞海?這個辦法不錯吧忌愚!成員變量默認初始化不創(chuàng)建對象,當調(diào)用Singleton.getInstance()
方法時却邓,如果sInstance
為null
再創(chuàng)建對象硕糊,否則就直接返回,保證了你的要求腊徙。
此時孫丞相“哼”了一下說简十,你這還不如趙學士呢!趙學士有可能提前白花錢聘請了一個管家撬腾,而你有可能多花錢請了好幾個管家呢螟蝙!你都沒有考慮到多線程的情況!錢大臣一聽趕緊做了修改民傻,代碼如下:
// 錢大臣的方案2
public class Singleton {
private static Singleton sInstance;
private Singleton() {
}
public synchronized static Singleton getInstance() {
if (sInstance == null) {
sInstance = new Singleton();
}
return sInstance;
}
}
給getInstance方法加了關(guān)鍵字synchronized胰默,保證創(chuàng)建對象的時候只有一個調(diào)用者,可以了吧漓踢?孫丞相又“哼”了一聲牵署,可每次調(diào)用的時候都會因為這個鎖帶來的時間開銷,你以為開鎖不要時間芭砦怼碟刺?性能低下!錢大臣臉有點紅薯酝,于是又做了修改:
// 錢大臣的方案3
public class Singleton {
private static Singleton sInstance;
private Singleton() {
}
public static Singleton getInstance() {
if (sInstance == null) {
synchronized (Singleton.class) {
sInstance = new Singleton();
}
}
return sInstance;
}
}
給sInstance = new Singleton();
語句加了鎖半沽,應(yīng)該沒問題了吧?孫丞相第三次“哼”了一聲吴菠,我給你假設(shè)個情況啊者填,設(shè)現(xiàn)有線程A和B,在某個時刻兩個線程都通過了判空語句但都沒有取到鎖資源做葵,然后線程A先取得鎖資源進入臨界區(qū)(被鎖的代碼塊)占哟,創(chuàng)建了一個對象,然后退出臨界區(qū)酿矢,釋放鎖資源榨乎。接著線程B取得鎖資源進入臨界區(qū),開始創(chuàng)建對象瘫筐,退出臨界區(qū)蜜暑,釋放鎖資源,請問現(xiàn)在有幾個Sinleton對象策肝?錢大臣聽后說那我直接把鎖加到判空語句之前肛捍!
// 錢大臣的方案4
public class Singleton {
private static Singleton sInstance;
private Singleton() {
}
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (sInstance == null) {
sInstance = new Singleton();
}
}
return sInstance;
}
}
孫丞相直接笑了隐绵,說你這樣和在方法上加synchronized關(guān)鍵字有什么區(qū)別。連續(xù)被懟拙毫,錢大臣感覺很沒面子直接反駁道依许,you can you up, no can no bb!
2.3 雙重校驗鎖DCL(double checked locking)
孫丞相大手一揮說道,看好了啊缀蹄,今兒讓我教教你怎么做人峭跳!
// 孫丞相的方案
public class Singleton {
private static volatile Singleton sInstance;
private Singleton() {
}
public static Singleton getInstance() {
if (sInstance == null) {
synchronized (Singleton.class) {
if (sInstance == null) {
sInstance = new Singleton();
}
}
}
return sInstance;
}
}
首先,方法鎖改成代碼塊鎖袍患,減少鎖的范圍坦康;其次第一次判空,在單線程的情況下提升了效率诡延,但此時如果同時存在兩個線程并發(fā)情況滞欠,即都判空成功,接下來會由鎖內(nèi)的第二次判空來過濾肆良。還是剛才的例子筛璧,假設(shè)現(xiàn)有線程A和B,在某個時刻兩個線程都通過了第一次判空語句但都沒有取到鎖資源惹恃。然后線程A先取得鎖資源進入臨界區(qū)(被鎖的代碼塊)夭谤,執(zhí)行第二次判空語句,判空成功巫糙,創(chuàng)建了一個對象朗儒,然后退出臨界區(qū)厢绝,釋放鎖資源烟零。接著線程B取得鎖資源進入臨界區(qū),執(zhí)行判空語句發(fā)現(xiàn)不通過龙亲,直接退出臨界區(qū)浙值,釋放鎖資源恳不。
另外在成員變量sInstance
前面加了一個volatile
關(guān)鍵字,這個特別重要开呐。容我裝個逼:在Java內(nèi)存模型(JMM)中烟勋,并不限制處理器的指令順序,說白了就是在不影響結(jié)果的情況下筐付,順序可能會被打亂卵惦。
在執(zhí)行sInstance = new Singleton();
這條命令語句時,JMM并不是一下就執(zhí)行完畢的瓦戚,即不是原子性
鸵荠,實質(zhì)上這句命令分為三大部分:
- 為對象分配內(nèi)存
- 執(zhí)行構(gòu)造方法語句,初始化實例對象
- 把sInstance的引用指向分配的內(nèi)存空間
在JMM中這三個步驟中的2和3不一定是順序執(zhí)行的伤极,如果線程A執(zhí)行的順序為1蛹找、3、2哨坪,在第2步執(zhí)行完畢的時候庸疾,恰好線程B執(zhí)行第一次判空語句,則會直接返回sInstance
当编,那么此時獲取到的sInstance
僅僅只是不為null
届慈,實質(zhì)上沒有初始化,這樣的對象肯定是有問題的忿偷!
而volatile
關(guān)鍵字的存在意義就是保證了執(zhí)行命令不會被重排序金顿,也就避免了這種異常情況的發(fā)生,所以這種獲取單例的方法才是真正的安全可靠鲤桥!
一直默默不做聲的李將軍冷不丁地開口了揍拆,孫丞相啊,你不覺得你這樣寫很麻煩嗎茶凳?我有更簡單的寫法呢嫂拴!
2.4 靜態(tài)內(nèi)部類實現(xiàn)的單例模式
你看你這又是判空又是加鎖的,多麻煩贮喧,其實可以通過靜態(tài)內(nèi)部類的方式筒狠,既保證了只存在一個單例,又保證了線程安全箱沦,代碼如下:
// 李將軍的方案
public class Singleton {
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.sInstance;
}
private static class SingletonHolder {
private static Singleton sInstance = new Singleton();
}
}
當外部類Singleton被加載時辩恼,其靜態(tài)內(nèi)部類SingeletonHolder不會被加載,所以它的成員變量sInstance是不會被初始化的谓形,只有當調(diào)用Singleton.getInstance()方法時灶伊,才會加載SingeletonHolder并且初始化其成員變量,而類加載時是線程安全的套耕,這樣既保證了延遲加載谁帕,也保證了線程安全,同時也簡化了代碼量冯袍,一舉三得匈挖!
2.5 枚舉單例
在說完上面4種單例模式的實現(xiàn)方式之后,不知道大家有沒有想到過一個問題康愤,那就是序列化儡循。我們可以通過以下代碼將實例寫入磁盤,然后再從磁盤讀出征冷,即使構(gòu)造方法是私有的择膝,反序列化也是可以通過特殊的途徑去重新創(chuàng)建一個新的實例,代碼如下:
public Singleton createNewInstance() throws IOException, ClassNotFoundException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
// 此處的singleton為通過單例模式獲取到的實例對象
oos.writeObject(singleton);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
// 此時返回一個反序列化后得到的新的實例對象
return (Singleton) ois.readObject();
}
可以通過上面的代碼看到检激,反序列化后可以得到一個新的實例對象肴捉,那么這種現(xiàn)象沒法避免了嗎腹侣?其實是可以避免的。反序列化提供了一個很特別的方法齿穗,即一個私有傲隶、被實例化的方法readResolve(),這個方法可以讓開發(fā)人員控制對象的反序列化窃页。想要杜絕上面現(xiàn)象的發(fā)生跺株,那么就可以在單例模式中加入readResolve()方法,代碼如下:
private Object readResolve() {
// 此處返回單例模式中的實例對象
return sInstance;
}
在《Effective Java》一書中脖卖,作者Joshua Bloch提倡可以采用枚舉的方式來解決上述出現(xiàn)的所有問題乒省,代碼如下:
// 外國老大哥Joshua Bloch的方案
public enum SingletonEnum {
INSTANCE;
public void method(){
// do something...
}
}
可以通過SingletonEnum.INSTANCE
獲取單例,然后再調(diào)用內(nèi)部的各種方法畦木。枚舉實現(xiàn)單例有如下好處:
- 實例的創(chuàng)建線程安全袖扛,確保單例;
- 防止被反射創(chuàng)建多個實例;
- 沒有序列化的問題。
雖然這種方法還沒有被廣泛采用馋劈,但是單元素的枚舉類型已經(jīng)成為實現(xiàn)Singleton單例模式的最佳方法攻锰。
3. 總結(jié)
通過上面的一步步分析,不知道大家有沒有對單例模式有個新的認識呢妓雾?總的來說娶吞,加了volatile關(guān)鍵字的雙重校驗鎖和靜態(tài)內(nèi)部類實現(xiàn)的單例模式是目前應(yīng)用最為廣泛的,如果你們要求更嚴的話械姻,那么枚舉單例也不失為一個獲取單例更加的方式妒蛇。歡迎各位能多多交流,指出不足楷拳,共同學習進步绣夺!