單例模式正確的開啟方式

概念

單例模式是一種常用的軟件設(shè)計模式缀踪。在應(yīng)用這個模式時滚局,單例對象的類必須保證只有一個實例存在工猜。本文就從單例模式的兩種構(gòu)建方式來了解一下單例。以下會給出多種單例的實現(xiàn)币狠,有正確的、也有存在缺陷的砾层。最后會總結(jié)各個方式優(yōu)缺點漩绵。

分類

  1. 餓漢式單例模式:指全局的單例實例在類加載時就主動創(chuàng)建實例。
  2. 懶漢式單例模式:指全局的單例實例在第一次被使用時才創(chuàng)建實例肛炮,不使用時不創(chuàng)建實例止吐。

實現(xiàn)方式

1。餓漢式:(記為 實現(xiàn)-1)

形象的描述就是“直接”侨糟,想象一下一名餓漢在吃東西時的樣子碍扔。食物到面前就開吃,簡單粗暴秕重。而代碼中的體現(xiàn)就是一被加載就構(gòu)建不同。實現(xiàn)起來也是簡單粗暴,沒有缺陷溶耘,唯一的不足就是耗費資源二拐。因為就算這個單例沒被使用到,它也會被實例化凳兵,占用內(nèi)存百新。

示例代碼

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

餓漢式的單例模式上面代碼就已經(jīng)實現(xiàn)了,我們平時使用時是這樣的:Singleton.getInstance();
當方法被調(diào)用時Singleton第一次被使用庐扫,此時類被加載饭望。類加載過程中靜態(tài)變量被初始化仗哨,instance 實例也就是在這時候被構(gòu)建。

2.懶漢式

懶漢式通俗的解釋起來就是铅辞,懶人干的事情厌漂,懶人做事就是需要做的時候才去做。在代碼上的體現(xiàn)就是延時加載巷挥。

下面來一步步從 缺陷完善 實現(xiàn)懶漢式單例模式:

  • 實現(xiàn) - 2 (存在缺陷的實現(xiàn))

我們經(jīng)常會寫以下代碼來實現(xiàn)單例模式桩卵,但是這種實現(xiàn)方式存在弊端:線程不安全。

示例代碼
public class Singleton {
    private final static Singleton instance;
    public static Singleton getInstance() {
        if (instance== null) {
            instance= new Singleton();
        }
        return instance;
    }
 }
我們來分析缺陷所在:

假設(shè)線程1倍宾、2同時調(diào)用getInstance()雏节,線程1準備執(zhí)行 instance= new Singleton(); 時被線程2預占。因為此時insteance 還未被示例話高职,所以線程2可以執(zhí)行完整個getInstance()方法钩乍,返回了Singleton對象引用。此時線程1在它停止的地方啟動怔锌,執(zhí)行接下來的代碼寥粹,由于已經(jīng)進行過了非空判斷,所以接下來就錯誤的再次實例化了一個Singleton對象埃元。此時就示例化了兩個Singleton對象涝涤。反之,如果能保證是單線程使用此單例對象岛杀,這種實現(xiàn)方式是沒有問題的阔拳。

  • 實現(xiàn) - 3 (對實現(xiàn) - 2進行改進)

實現(xiàn)2中既然存在線程不安全的問題,那么很容易就想到一個處理方法类嗤,那就是加鎖糊肠。

示例代碼
public class Singleton {
    private final static Singleton instance;
    public static synchronized Singleton getInstance() {
        if (instance== null) {
            instance= new Singleton();
        }
        return instance;
    }
}

這種實現(xiàn)與 實現(xiàn)2 相比較,差別就在于一個同步鎖遗锣。加了鎖的getInstance() 可以保證線程安全货裹,并且也實現(xiàn)了單例。
這一種正確的單例實現(xiàn)方式精偿,但是由于對 getInstance()做了同步處理弧圆,synchronized將導致性能開銷。

分析這種實現(xiàn)發(fā)現(xiàn)其實只有在第一次調(diào)用方法時才需要同步笔咽。(此處自行理解下)

由于只有第一次調(diào)用執(zhí)行了 instance= new Singleton()墓阀,而只有此行代碼需要同步,因此就無需對后續(xù)調(diào)用使用同步拓轻。除了第一次調(diào)用外其他的調(diào)用都只需要判斷 instance是否為 null斯撮,并將其返回。多線程能夠安全并發(fā)地執(zhí)行除第一次調(diào)用外的所有調(diào)用扶叉。
由于該方法是synchronized 的勿锅,需要為該方法的每一次調(diào)用付出同步的代價帕膜,即使只有第一次調(diào)用需要同步。所以如果getInstance()被多個線程頻繁的調(diào)用溢十,將會導致程序執(zhí)行性能的下降垮刹。反之,如果getInstance()不會被多個線程頻繁的調(diào)用张弛,那么這個延遲初始化方案將能提供令人滿意的性能荒典。

既然有性能上的不足,那么我們偉大的程序猿自然會想出優(yōu)化性能的方法吞鸭。所以就有了接下的的這種實現(xiàn) 雙重檢查鎖定

  • 實現(xiàn) - 4 (對實現(xiàn) - 3進行性能優(yōu)化后的實現(xiàn) - 雙重檢查鎖定 double-checked locking)

先聲明寺董,雙重檢查鎖定這種實現(xiàn)方式是一種存在漏洞的單例實現(xiàn)
示例代碼
public class Singleton {
    private final static Singleton instance;
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance== null) {
                    instance= new Singleton(); // 問題出現(xiàn)位置
                }
            }
        }
        return instance;
    }
}

上面的代碼就是 雙重檢查鎖定的實現(xiàn)方式。

分析 實現(xiàn) - 4 的代碼:

如果第一次檢查instance不為null刻剥,那么就不需要執(zhí)行下面的加鎖和初始化操作遮咖。因此可以大幅降低synchronized帶來的性能開銷。上面代碼表面上看起來造虏,似乎兩全其美:

1. 在多個線程情況下同一時間調(diào)用getInstance()時御吞,會通過加鎖來保證只有一個線程能創(chuàng)建對象。
2.在對象創(chuàng)建好之后漓藕,執(zhí)行getInstance()將不需要每次都獲取鎖陶珠,直接返回已創(chuàng)建好的對象,優(yōu)化了實現(xiàn)-3 中多次獲取鎖導致的性能消耗享钞。

雙重檢查鎖定看起來似乎很完美揍诽,但這是一個錯誤的優(yōu)化!在線程執(zhí)行到第4行代碼讀取到instance不為null時嫩与,instance引用的對象有可能還沒有完成初始化。

該問題的具體分析請看這里 :http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization

我在這里做簡單的分析并給出解決方式

前面的雙重檢查鎖定示例代碼的第7行instance = new Singleton()創(chuàng)建一個對象交排。這一行代碼可以分解為如下的三行偽代碼:

memory = allocate();   //1:分配對象的內(nèi)存空間
ctorInstance(memory);  //2:初始化對象
instance = memory;     //3:設(shè)置instance指向剛分配的內(nèi)存地址

上面三行偽代碼中的2和3之間划滋,可能會被重排序(在一些JIT編譯器上,這種重排序是真實發(fā)生的埃篓,詳情見參考文獻1的“Out-of-order writes”部分)处坪。2和3之間重排序之后的執(zhí)行時序如下:

memory = allocate();   //1:分配對象的內(nèi)存空間
instance = memory;     //3:設(shè)置instance指向剛分配的內(nèi)存地址
                       //注意,此時對象還沒有被初始化架专!
ctorInstance(memory);  //2:初始化對象

根據(jù)《The Java Language Specification, Java SE 7 Edition》(后文簡稱為java語言規(guī)范)同窘,所有線程在執(zhí)行java程序時必須要遵守intra-thread semantics。intra-thread semantics保證重排序不會改變單線程內(nèi)的程序執(zhí)行結(jié)果部脚。換句話來說想邦,intra-thread semantics允許那些在單線程內(nèi),不會改變單線程程序執(zhí)行結(jié)果的重排序委刘。上面三行偽代碼的2和3之間雖然被重排序了丧没,但這個重排序并不會違反intra-thread semantics鹰椒。這個重排序在沒有改變單線程程序的執(zhí)行結(jié)果的前提下,可以提高程序的執(zhí)行性能呕童。

為了更好的理解intra-thread semantics漆际,請看下面的示意圖(假設(shè)一個線程A在構(gòu)造對象后,立即訪問這個對象):


重排序圖示.png

如上圖所示夺饲,只要保證2排在4的前面奸汇,即使2和3之間重排序了,也不會違反intra-thread semantics往声。
下面擂找,再讓我們看看多線程并發(fā)執(zhí)行的時候的情況。請看下面的示意圖:


多線程重排序圖示.png

上圖標識什么意思呢烁挟?
由于單線程內(nèi)要遵守intra-thread semantics婴洼,從而能保證A線程的程序執(zhí)行結(jié)果不會被改變。但是當線程A和B按上圖的時序執(zhí)行時撼嗓,B線程將看到一個還沒有被初始化的對象柬采。
這么說有點抽象,我們回到代碼分析

示例代碼第七行 instance= new Singleton() 且警,此處若是發(fā)生重排序粉捻,對象還未被初始化完成。此時另一個并發(fā)的線程B就有可能在 第4行判斷 instance 不為 null 斑芜。那么線程B就將訪問未完成初始化的對象肩刃。這就是錯誤所在。

在知曉了問題發(fā)生的根源之后杏头,我們可以想出兩個辦法來實現(xiàn)線程安全的延遲初始化:
1. 不允許2和3重排序盈包;

2. 允許2和3重排序,但不允許其他線程“看到”這個重排序醇王。

既然想到了方法呢燥,那么就用代碼來實現(xiàn)。

  • 解決方案1:實現(xiàn) - 5 (基于volatile的雙重檢查鎖定)

示例代碼
public class Instance {
    private volatile static Instance instance;

    private Instance (){ }

    public static Instance getInstance() {
        if (instance== null) {
            synchronized (SafeDoubleCheckedLocking.class) {
                if (instance== null)
                    instance= new Instance();//instance 為volatile寓娩,現(xiàn)在沒問題了
            }
        }
        return instance;
    }
}
注意叛氨,這個解決方案需要JDK5或更高版本(因為從JDK5開始使用新的JSR-133內(nèi)存模型規(guī)范,這個規(guī)范增強了volatile的語義)棘伴。當聲明對象的引用為volatile后寞埠,“問題的根源”的三行偽代碼中的2和3之間的重排序,在多線程環(huán)境中將會被禁止焊夸。禁止后仁连,線程B在進行第一次 instance == null 判斷時就不會為true, 將按如下的時序執(zhí)行:
image.png

這個方案本質(zhì)上是通過禁止上圖中的2和3之間的重排序阱穗,來保證線程安全的延遲初始化怖糊。

解決方案2:實現(xiàn) - 6(基于類初始化的解決方案 - Initialization On Demand Holder idiom)

示例代碼
public class Singleton {   
    private Singleton() {
    }
    private static class LazyHolder {
        static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE; // 這里將導致Singleton類被初始化
    }
}

JVM在類的初始化階段(即在Class被加載后帅容,且被線程使用之前),會執(zhí)行類的初始化伍伤。在執(zhí)行類的初始化期間并徘,JVM會去獲取一個鎖。這個鎖可以同步多個線程對同一個類的初始化扰魂。相比其他實現(xiàn)方案(如double-checked locking等)麦乞,該技術(shù)方案的實現(xiàn)代碼較為簡潔,并且在所有版本的編譯器中都是可行的劝评。

補充內(nèi)容

關(guān)于實現(xiàn) - 6中姐直,static final Instance instance 域的訪問權(quán)限為什么是包級私有可以讀:Initialization On Demand Holder idiom的實現(xiàn)探討

各種實現(xiàn)方式的優(yōu)缺點:

  • 餓漢式(實現(xiàn) - 1) 單例實例在類裝載時就構(gòu)建,急切初始化蒋畜。
    • 優(yōu)點:
      • 線程安全
      • 在類加載的同時已經(jīng)創(chuàng)建好一個靜態(tài)對象声畏,調(diào)用時反應(yīng)速度快。
    • 缺點
      • 資源效率不高姻成,getInstance()可能永遠不會執(zhí)行到插龄,但執(zhí)行該類的其他靜態(tài)方法或者加載了該類(class.forName),那么這個實例仍然初始化科展。
  • 懶漢式 (實現(xiàn) 2 - 6)單例實例在第一次被使用時構(gòu)建(調(diào)用 getInsteance())均牢,延遲初始化。
    • 實現(xiàn) - 2(缺陷實現(xiàn)):這種實現(xiàn)方式存在線程不安全的缺陷才睹,不推薦使用徘跪。但若能保證處于單線程中,可以使用這種實現(xiàn)方式琅攘。
    • 實現(xiàn) - 3(耗資源實現(xiàn)):這種實現(xiàn)方式對實現(xiàn)-2中線程不安全的缺陷進行了處理垮庐。
      • 優(yōu)點:資源利用率高,不執(zhí)行g(shù)etInstance()就不會被實例坞琴,可以執(zhí)行該類的其他靜態(tài)方法哨查。
      • 缺點:第一次加載時不夠快,多線程使用不必要的同步開銷大
    • 實現(xiàn) - 4(問題實現(xiàn)):雙重檢查鎖定置济,這是對實現(xiàn) - 3的一種優(yōu)化實現(xiàn)解恰,但是存在重排序?qū)е芦@取到未初始化的單例對象的問題
    • 實現(xiàn) - 5:雙重檢查鎖定+volatile
      • 優(yōu)點:資源利用率高锋八,不執(zhí)行g(shù)etInstance()就不會被實例浙于,可以執(zhí)行該類的其他靜態(tài)方法。
      • 缺點:第一次加載時反應(yīng)不快挟纱。實現(xiàn)起來代碼較為復雜羞酗。
      • 注意點:jdk1.5版本后volatile關(guān)鍵字才能正確的工作。Android平臺不同當心這個問題紊服,一般Android都是jdk1.6以上檀轨。
    • 實現(xiàn) - 6:靜態(tài)內(nèi)部類
      • 優(yōu)點:資源利用率高胸竞,不執(zhí)行g(shù)etInstance()就不會被實例,可以執(zhí)行該類的其他靜態(tài)方法参萄。實現(xiàn)代碼較為簡潔
      • 缺點:第一次加載時反應(yīng)不快卫枝。

總結(jié):

  1. 延遲初始化降低了初始化類或創(chuàng)建實例的開銷,但增加了訪問被延遲初始化的字段的開銷(鎖)讹挎。在大多數(shù)時候校赤,正常的初始化要優(yōu)于延遲初始化。
  2. 如果確實需要對實例字段使用線程安全的延遲初始化筒溃,請使用上面介紹的基于volatile的延遲初始化的方案(實現(xiàn) - 5)马篮;
  3. 如果確實需要對靜態(tài)字段使用線程安全的延遲初始化,請使用上面介紹的基于類初始化的方案怜奖。(實現(xiàn) - 6)
  4. 一般采用餓漢式(實現(xiàn) - 1)浑测,若對資源十分在意建議采用靜態(tài)內(nèi)部類(實現(xiàn) - 6)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末歪玲,一起剝皮案震驚了整個濱河市迁央,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌读慎,老刑警劉巖漱贱,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異夭委,居然都是意外死亡幅狮,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門株灸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來崇摄,“玉大人,你說我怎么就攤上這事慌烧≈鹨郑” “怎么了?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵屹蚊,是天一觀的道長厕氨。 經(jīng)常有香客問我,道長汹粤,這世上最難降的妖魔是什么命斧? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮嘱兼,結(jié)果婚禮上国葬,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好汇四,可當我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布接奈。 她就那樣靜靜地躺著,像睡著了一般通孽。 火紅的嫁衣襯著肌膚如雪序宦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天背苦,我揣著相機與錄音挨厚,去河邊找鬼。 笑死糠惫,一個胖子當著我的面吹牛失暂,可吹牛的內(nèi)容都是我干的作谭。 我是一名探鬼主播要出,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼捷兰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了固阁?” 一聲冷哼從身側(cè)響起壤躲,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎备燃,沒想到半個月后碉克,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡并齐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年漏麦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片况褪。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡撕贞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出测垛,到底是詐尸還是另有隱情捏膨,我是刑警寧澤,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布食侮,位于F島的核電站号涯,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏锯七。R本人自食惡果不足惜链快,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望起胰。 院中可真熱鬧久又,春花似錦、人聲如沸效五。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽畏妖。三九已至脉执,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間戒劫,已是汗流浹背半夷。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留迅细,地道東北人巫橄。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像茵典,于是被迫代替她去往敵國和親湘换。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 老夫記事六十年统阿,小時過年在眼前彩倚。夾生餃子急入口,一片大肉香半天扶平。稚兒不圖衣多好帆离,手上爆竹霸王鞭。娃子群里逞能干结澄,點...
    sx老魚閱讀 96評論 0 0
  • Chapter 45 再見哥谷,火柴 當然,那電話在正常不過了麻献,不是靈異事件呼巷,而是個妹子。她追了我有段時間了赎瑰,可能是因...
    風揚川閱讀 469評論 0 3
  • 在工地和工人打交道是必不可少的王悍,可以說這對施工管理至關(guān)重要,去年差不多也是這個是老黃(包工頭餐曼,我問過他年齡有四十幾...
    放羊的小娃娃閱讀 183評論 2 2
  • 寶貝囡
    Biu你死了閱讀 108評論 0 0