設計模式02_單例模式_Singleton

1.什么情況下使用單例模式痴腌?

有些對象只有一個,比如配置文件燃领,工具類士聪,線程池,緩存猛蔽,日志對象等等剥悟。單例模式保證應用中有且只有一個實例。

2. 什么是單例曼库?

2.1区岗、單例定義

“單例對象的類必須保證只有一個實例存在” 這是維基百科上對單例的定義,這也可以作為對意圖實現(xiàn)單例模式的代碼進行檢驗的標準毁枯。

2.2慈缔、單例的實現(xiàn)可以分為兩大類

懶漢式:指全局的單例實例在第一次被使用時構建。
餓漢式:指全局的單例實例在類裝載時構建种玛。
注:日常我們使用的較多的應該是懶漢式的單例胀糜,畢竟按需加載才能做到資源的最大化利用。

3. 懶漢式單例

先來看一下懶漢式單例的實現(xiàn)方式蒂誉。

3.1 簡單版本

看最簡單的寫法Version 1:

public class LazySingleton {
    //1. Simplest version
    private static LazySingleton instance;
    private LazySingleton(){}
    public static LazySingleton getInstance(){
        if(instance == null){
            instance =  new LazySingleton();
        }
        return instance;
    }
}

把構造器改為私有的,這樣能夠防止被外部的類調用距帅。每次獲取instance之前先進行判斷右锨,如果instance為空就new一個出來,否則就直接返回已存在的instance碌秸。這種寫法在大多數(shù)的時候也是沒問題的绍移。問題在于悄窃,當多線程工作的時候,如果有多個線程同時運行到if (instance == null)蹂窖,都判斷為null,那么兩個線程就各自會創(chuàng)建一個實例——這樣一來,就不是單例了端蛆。

3.2 synchronized版本

那既然可能會因為多線程導致問題床三,那么加上一個同步鎖吧!修改后的代碼如下月趟,相對于Version1灯蝴,只是在方法簽名上多加了一個synchronized:

//2. Sychronized version
    private static LazySingleton instance2;
    private LazySingleton(){}
    public static synchronized LazySingleton getInstance2(){
        if(instance2 == null){
            instance2 = new LazySingleton();
        }
        return instance2;
    }

OK,加上synchronized關鍵字之后孝宗,getInstance方法就會鎖上了穷躁。如果有兩個線程(T1、T2)同時執(zhí)行到這個方法時因妇,會有其中一個線程T1獲得同步鎖问潭,得以繼續(xù)執(zhí)行,而另一個線程T2則需要等待婚被,當?shù)赥1執(zhí)行完畢getInstance之后(完成了null判斷狡忙、對象創(chuàng)建、獲得返回值之后)摔寨,T2線程才會執(zhí)行執(zhí)行去枷。——所以這端代碼也就避免了Version1中是复,可能出現(xiàn)因為多線程導致多個實例的情況删顶。但是,這種寫法也有一個問題:給getInstance方法加鎖淑廊,雖然會避免了可能會出現(xiàn)的多個實例問題逗余,但是會強制除T1之外的所有線程等待,實際上會對程序的執(zhí)行效率造成負面影響季惩。

3.3 雙重檢查(Double-Check)版本

Version2代碼相對于Version1d代碼的效率問題录粱,其實是為了解決1%幾率的問題,而使用了一個100%出現(xiàn)的防護盾画拾。那有一個優(yōu)化的思路啥繁,就是把100%出現(xiàn)的防護盾,也改為1%的幾率出現(xiàn)青抛,使之只出現(xiàn)在可能會導致多個實例出現(xiàn)的地方旗闽。——有沒有這樣的方法呢?當然是有的适室,改進后的代碼Vsersion3如下:

    //3. Double-check version
    private static LazySingleton instance3;
    private LazySingleton(){}
    public static LazySingleton getInstance3(){
        if(instance3 == null){
            synchronized (LazySingleton.class){
                if(instance3 == null){
                    instance3 = new LazySingleton();
                }
            }
        }
        return instance3;
    }

第一個if (instance == null)嫡意,其實是為了解決Version2中的效率問題,只有instance為null的時候捣辆,才進入synchronized的代碼段大大減少了幾率蔬螟。
第二個if (instance == null),則是跟Version2一樣汽畴,是為了防止可能出現(xiàn)多個實例的情況旧巾。
這段代碼看起來已經完美無瑕了≌………………—— 當然菠齿,只是『看起來』,還是有小概率出現(xiàn)問題的坐昙。這弄清楚為什么這里可能出現(xiàn)問題绳匀,首先,我們需要弄清楚幾個概念:原子操作炸客、指令重排疾棵。
主要在于singleton = new Singleton()這句,這并非是一個原子操作痹仙,事實上在 JVM 中這句話大概做了下面 3 件事情是尔。
  1. 給 singleton 分配內存
  2. 調用 Singleton 的構造函數(shù)來初始化成員變量,形成實例
  3. 將singleton對象指向分配的內存空間(執(zhí)行完這步 singleton才是非 null 了)但是在 JVM 的即時編譯器中存在指令重排序的優(yōu)化开仰。
  也就是說上面的第二步和第三步的順序是不能保證的拟枚,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2。如果是后者众弓,則在 3 執(zhí)行完畢恩溅、2 未執(zhí)行之前,被線程二搶占了谓娃,這時 instance 已經是非 null 了(但卻沒有初始化)脚乡,所以線程二會直接返回 instance,然后使用滨达,然后順理成章地報錯奶稠。
  再稍微解釋一下,就是說捡遍,由于有一個『instance已經不為null但是仍沒有完成初始化』的中間狀態(tài)锌订,而這個時候,如果有其他線程剛好運行到第一層if (instance == null)這里画株,這里讀取到的instance已經不為null了瀑志,所以就直接把這個中間狀態(tài)的instance拿去用了涩搓,就會產生問題。這里的關鍵在于——線程T1對instance的寫操作沒有完成劈猪,線程T2就執(zhí)行了讀操作。

3.4 終極版本:volatile

對于Version3中可能出現(xiàn)的問題(當然這種概率已經非常小了良拼,但畢竟還是有的嘛~)战得,解決方案是:只需要給instance的聲明加上volatile關鍵字即可,Version4版本:

//4. Double-check with volatile version
    private static volatile LazySingleton instance4;
    private LazySingleton(){}
    public static LazySingleton getInstance4(){
        if(instance4 == null){
            synchronized (LazySingleton.class){
                if(instance4 == null){
                    instance4 = new LazySingleton();
                }
            }
        }
        return instance4;
    }

一旦一個共享變量(類的成員變量庸推、類的靜態(tài)成員變量)被volatile修飾之后常侦,那么就具備了兩層語義:

1)可見性:保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值贬媒,這新值對其他線程來說是立即可見的聋亡。

2)有序性:禁止進行指令重排序。
我的理解是际乘,volatile修飾后坡倔,保證了singleton = new Singleton()這句話的指令執(zhí)行順序,從而不會出現(xiàn)版本3的問題脖含。

4. 餓漢式單例

下面再聊了解一下餓漢式的單例罪塔。
  如上所說,餓漢式單例是指:指全局的單例實例在類裝載時構建的實現(xiàn)方式养葵。
由于類裝載的過程是由類加載器(ClassLoader)來執(zhí)行的征堪,這個過程也是由JVM來保證同步的,所以這種方式先天就有一個優(yōu)勢——能夠免疫許多由多線程引起的問題关拒。

4.1 餓漢式單例的實現(xiàn)方式

餓漢式單例的實現(xiàn)如下:

public class HungreySingleton {
    private static final HungreySingleton instance = new HungreySingleton();
    private HungreySingleton(){}
    public static HungreySingleton getInstance() {
        return instance;
    }
}

對于一個餓漢式單例的寫法來說佃蚜,它基本上是完美的了。所以它的缺點也就只是餓漢式單例本身的缺點所在了——由于INSTANCE的初始化是在類加載時進行的着绊,而類的加載是由ClassLoader來做的谐算,所以開發(fā)者本來對于它初始化的時機就很難去準確把握:可能由于初始化的太早,造成資源的浪費畔柔。
  如果初始化本身依賴于一些其他數(shù)據(jù)氯夷,那么也就很難保證其他數(shù)據(jù)會在它初始化之前準備好。
  當然靶擦,如果所需的單例占用的資源很少腮考,并且也不依賴于其他數(shù)據(jù),那么這種實現(xiàn)方式也是很好的玄捕。

4.2什么時候是類裝載時踩蔚?

類從被加載到虛擬機內存中開始,直到卸載出內存為止枚粘,它的整個生命周期包括了:加載馅闽、驗證、準備、解析福也、初始化局骤、使用和卸載這7個階段。其中暴凑,驗證峦甩、準備和解析這三個部分統(tǒng)稱為連接(linking)。
什么情況下需要開始類加載過程的第一個階段:"加載"现喳。虛擬機規(guī)范中并沒強行約束凯傲,這點可以交給虛擬機的的具體實現(xiàn)自由把握,但是對于初始化階段虛擬機規(guī)范是嚴格規(guī)定了如下幾種情況嗦篱,如果類未初始化會對類進行初始化冰单。

  • 創(chuàng)建類的實例
  • 訪問類的靜態(tài)變量(除常量【被final修辭的靜態(tài)變量】原因:常量一種特殊的變量,因為編譯器把他們當作值(value)而不是域(field)來對待灸促。如果你的代碼中用到了常變量(constant variable)诫欠,編譯器并不會生成字節(jié)碼來從對象中載入域的值,而是直接把這個值插入到字節(jié)碼中腿宰。這是一種很有用的優(yōu)化呕诉,但是如果你需要改變final域的值那么每一塊用到那個域的代碼都需要重新編譯。
  • 訪問類的靜態(tài)方法
  • 反射如(Class.forName("my.xyz.Test"))
  • 當初始化一個類時吃度,發(fā)現(xiàn)其父類還未初始化甩挫,則先出發(fā)父類的初始化
  • 虛擬機啟動時,定義了main()方法的那個類先初始化
class SingleTon {
    private static SingleTon singleTon = new SingleTon();
    public static int count1;
    public static int count2 = 0;
 
    private SingleTon() {
        count1++;
        count2++;
    }
 
    public static SingleTon getInstance() {
        return singleTon;
    }
}
 
public class Test {
    public static void main(String[] args) {
        SingleTon singleTon = SingleTon.getInstance();
        System.out.println("count1=" + singleTon.count1);
        System.out.println("count2=" + singleTon.count2);
    }
}
輸出:
count1=1
count2=0
分析:
1:SingleTon singleTon = SingleTon.getInstance();調用了類的SingleTon調用了類的靜態(tài)方法椿每,觸發(fā)類的初始化
2:類加載的時候在準備過程中為類的靜態(tài)變量分配內存并初始化默認值 singleton=null count1=0,count2=0
3:類初始化伊者,為類的靜態(tài)變量賦值和執(zhí)行靜態(tài)代碼快。singleton賦值為new SingleTon()調用類的構造方法
4:調用類的構造方法后count=1;count2=1
5:繼續(xù)為count1與count2賦值,此時count1沒有賦值操作,所有count1為1,但是count2執(zhí)行賦值操作就變?yōu)?

5. 一些其他的實現(xiàn)方式

5.1 Effective Java 1 —— 靜態(tài)內部類

《Effective Java》一書的第一版中推薦了一個中寫法:

public class InnerSingleton {
    private static class SingletonHolder{
        private static final InnerSingleton instance = new InnerSingleton();
    }
    private InnerSingleton(){}
    public static final InnerSingleton getInstance(){
        return SingletonHolder.instance;
    }
}

這種寫法非常巧妙:對于內部類SingletonHolder间护,它是一個餓漢式的單例實現(xiàn)亦渗,在SingletonHolder初始化的時候會由ClassLoader來保證同步,使INSTANCE是一個真·單例汁尺。
  同時法精,由于SingletonHolder是一個內部類,只在外部類的Singleton的getInstance()中被使用痴突,所以它被加載的時機也就是在getInstance()方法第一次被調用的時候搂蜓。
  它利用了ClassLoader來保證了同步,同時又能讓開發(fā)者控制類加載的時機辽装。從內部看是一個餓漢式的單例帮碰,但是從外部看來,又的確是懶漢式的實現(xiàn)**拾积。簡直是神乎其技殉挽。

5.2 Effective Java 2 —— 枚舉

《Effective Java》的作者在這本書的第二版又推薦了另外一種方法丰涉,來直接看代碼:

public enum SingleInstance {
   INSTANCE;
    public void fun1() {
        // do something
    }
}// 使用SingleInstance.INSTANCE.fun1();

看到了么?這是一個枚舉類型……連class都不用了斯碌,極簡一死。由于創(chuàng)建枚舉實例的過程是線程安全的,所以這種寫法也沒有同步的問題傻唾。

對這個方法的評價:
  這種寫法在功能上與共有域方法相近摘符,但是它更簡潔,無償?shù)靥峁┝诵蛄谢瘷C制策吠,絕對防止對此實例化,即使是在面對復雜的序列化或者反射攻擊的時候瘩绒。雖然這中方法還沒有廣泛采用猴抹,但是單元素的枚舉類型已經成為實現(xiàn)Singleton的最佳方法。
  枚舉單例這種方法問世一些锁荔,許多分析文章都稱它是實現(xiàn)單例的最完美方法——寫法超級簡單蟀给,而且又能解決大部分的問題。不過我個人認為這種方法雖然很優(yōu)秀阳堕,但是它仍然不是完美的——比如跋理,在需要繼承的場景,它就不適用了恬总。

6. 總結

OK前普,看到這里,你還會覺得單例模式是最簡單的設計模式了么壹堰?再回頭看一下你之前代碼中的單例實現(xiàn)拭卿,覺得是無懈可擊的么?可能我們在實際的開發(fā)中贱纠,對單例的實現(xiàn)并沒有那么嚴格的要求峻厚。比如,我如果能保證所有的getInstance都是在一個線程的話谆焊,那其實第一種最簡單的教科書方式就夠用了惠桃。再比如,有時候辖试,我的單例變成了多例也可能對程序沒什么太大影響……但是辜王,如果我們能了解更多其中的細節(jié),那么如果哪天程序出了些問題剃执,我們起碼能多一個排查問題的點誓禁。早點解決問題,就能早點回家吃飯……:-D
   還有肾档,完美的方案是不存在摹恰,任何方式都會有一個『度』的問題辫继。比如,你的覺得代碼已經無懈可擊了俗慈,但是因為你用的是JAVA語言姑宽,可能ClassLoader有些BUG啊……你的代碼誰運行在JVM上的,可能JVM本身有BUG啊……你的代碼運行在手機上闺阱,可能手機系統(tǒng)有問題啊……你生活在這個宇宙里炮车,可能宇宙本身有些BUG啊……o(╯□╰)o所以,盡力做到能做到的最好就行了酣溃。
   感謝你花費了不少時間看到這里瘦穆,但愿你沒有覺得虛度。

本文僅對于原文作少許修改赊豌。
原文:
作者:博麟K
鏈接:http://www.reibang.com/p/d2755af464d2

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末扛或,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子碘饼,更是在濱河造成了極大的恐慌熙兔,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件艾恼,死亡現(xiàn)場離奇詭異住涉,居然都是意外死亡,警方通過查閱死者的電腦和手機钠绍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門舆声,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人五慈,你說我怎么就攤上這事纳寂。” “怎么了泻拦?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵毙芜,是天一觀的道長。 經常有香客問我争拐,道長腋粥,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任架曹,我火速辦了婚禮隘冲,結果婚禮上,老公的妹妹穿的比我還像新娘绑雄。我一直安慰自己展辞,他們只是感情好,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布万牺。 她就那樣靜靜地躺著罗珍,像睡著了一般洽腺。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上覆旱,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天蘸朋,我揣著相機與錄音,去河邊找鬼扣唱。 笑死藕坯,一個胖子當著我的面吹牛,可吹牛的內容都是我干的噪沙。 我是一名探鬼主播炼彪,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼正歼!你這毒婦竟也來了霹购?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤朋腋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后膜楷,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體旭咽,經...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年赌厅,在試婚紗的時候發(fā)現(xiàn)自己被綠了穷绵。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡特愿,死狀恐怖仲墨,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情揍障,我是刑警寧澤目养,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站毒嫡,受9級特大地震影響癌蚁,放射性物質發(fā)生泄漏。R本人自食惡果不足惜兜畸,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一努释、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧咬摇,春花似錦伐蒂、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽恩沛。三九已至,卻和暖如春昭雌,著一層夾襖步出監(jiān)牢的瞬間复唤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工烛卧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留佛纫,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓总放,卻偏偏與公主長得像呈宇,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子局雄,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內容