深入理解單例模式的設(shè)計與實現(xiàn)

單例模式(Singleton Pattern)是 Java 中相對簡單的設(shè)計模式之一狸涌。這種類型的設(shè)計模式屬于創(chuàng)建型模式旦万,它提供了一種創(chuàng)建對象的最佳方式橄仆。

這種模式涉及到一個單一的類歌豺,該類負(fù)責(zé)創(chuàng)建自己的對象桅滋,同時確保只有單個對象被創(chuàng)建。這個類提供了一種訪問其唯一的對象的方式既绩,可以直接訪問概龄,不需要實例化該類的對象。


使用場景

1饲握、要求生產(chǎn)唯一序列號私杜。
2、WEB 中的計數(shù)器救欧,不用每次刷新都在數(shù)據(jù)庫里加一次衰粹,用單例先緩存起來。
3笆怠、創(chuàng)建的一個對象需要消耗的資源過多铝耻,比如 I/O 與數(shù)據(jù)庫的連接等。

注意事項:getInstance() 方法中需要使用同步鎖 synchronized (Singleton.class) 防止多線程同時進入造成 instance 被多次實例化蹬刷。

實現(xiàn)1:懶漢式瓢捉,線程不安全

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

分析:這種方式是最容易想到的實現(xiàn)方式,但是這種實現(xiàn)存在一個很大大的問題:不支持多線程办成。因為沒有加鎖 synchronized泡态,所以嚴(yán)格意義上它并不算單例模式。
這種方式 lazy loading 很明顯迂卢,不要求線程安全某弦,在多線程不能正常工作。


實現(xiàn)2:懶漢式冷守,線程安全

基于實現(xiàn)1存在的線程不安全問題,可得到以下實現(xiàn):

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

分析:這種方式具備很好的 lazy loading惊科,能夠在多線程中很好的工作拍摇,但是,效率很低馆截,大多數(shù)情況下不需要同步充活。

優(yōu)點:第一次調(diào)用才初始化蜂莉,避免內(nèi)存浪費。
缺點:必須加鎖 synchronized 才能保證單例混卵,但加鎖會影響效率映穗。getInstance() 的性能對應(yīng)用程序不是很關(guān)鍵(該方法使用不太頻繁)。


實現(xiàn)3:懶漢式幕随,雙重檢驗鎖蚁滋,線程安全,性能較高

基于實現(xiàn)2中的性能問題赘淮,可得出以下線程安全且高性能的實現(xiàn):

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    } 
 }

分析:這種方式采用雙鎖機制辕录,即通過volatile關(guān)鍵字保證線程的內(nèi)存可見,確保singleton是否為null能及時刷新到各線程的工作內(nèi)存中梢卸,同時有在if條件內(nèi)部添加synchronized同步鎖安全走诞,在singleton實例為空時,才給線程添加同步鎖蛤高,以保證線程安全蚣旱,且在多線程情況下能保持高性能。


實現(xiàn)4:餓漢式戴陡,線程安全

基于實現(xiàn)3中復(fù)雜的實現(xiàn)方式塞绿,可采用實現(xiàn)更為簡單的寫法,如下:

public Singleton(){
    private static final Singleton singleton = new Singleton();
    
    private Singleton(){}

    public static Singleton getInstance(){
        return singleton;
    }

}

分析:這種方式比較常用猜欺,但容易產(chǎn)生垃圾對象位隶。
優(yōu)點:沒有加鎖,執(zhí)行效率會提高开皿。
缺點:類加載時就初始化涧黄,浪費內(nèi)存。
深入理解:它基于 classloder 機制避免了多線程的同步問題赋荆,即笋妥,類初始化階段由JLS保證是順序的,是非并發(fā)的窄潭,不過春宣,instance 在類裝載時就實例化,雖然導(dǎo)致類裝載的原因有很多種嫉你,在單例模式中大多數(shù)都是調(diào)用 getInstance 方法月帝, 但是也不能確定有其他的方式(或者其他的靜態(tài)方法)導(dǎo)致類裝載,這時候初始化 instance 顯然沒有達到 lazy loading 的效果幽污。


實現(xiàn)5:靜態(tài)內(nèi)部類法嚷辅,懶加載,線程安全

基于實現(xiàn)4中存在的資源耗費問題距误,可有以下解決方法來改善實現(xiàn):

public class Singleton{
    private  Singleton(){}
    //創(chuàng)建內(nèi)部靜態(tài)類簸搞,第一次使用時才被加載
    private static class SingletonHolder{
        private static final Singleton instance = new Singleton();
    }

    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }

}

分析:這種方式能達到雙檢鎖方式一樣的功效扁位,但實現(xiàn)更簡單。對靜態(tài)域使用延遲初始化趁俊,應(yīng)使用這種方式而不是雙檢鎖方式域仇。這種方式只適用于靜態(tài)域的情況,雙檢鎖方式可在實例域需要延遲初始化時使用寺擂。

深入理解:這種方式同樣利用了 classloder 機制來保證初始化 instance 時只有一個線程暇务,它跟第 4 種方式不同的是:第 4 種方式只要 Singleton 類被裝載了,那么 instance 就會被實例化(沒有達到 lazy loading 效果)沽讹,而這種方式是 Singleton 類被裝載了般卑,instance 不一定被初始化。因為 SingletonHolder 類沒有被主動使用爽雄,只有顯示通過調(diào)用 getInstance 方法時蝠检,才會顯示裝載 SingletonHolder 類,從而實例化 instance挚瘟。
試想叹谁,如果實例化 instance 很消耗資源,所以想讓它延遲加載乘盖,另外一方面焰檩,又不希望在 Singleton 類加載時就實例化,因為不能確保 Singleton 類還可能在其他的地方被主動使用從而被加載订框,那么這個時候?qū)嵗?instance 顯然是不合適的析苫。這個時候,這種方式相比第 4 種方式就顯得很合理穿扳。

延伸:虛擬機規(guī)范嚴(yán)格規(guī)定了5種情況下衩侥,必須對類(注意是類)進行初始化

  1. 遇到new,getstatic,putstatic,invokestatic這4條字節(jié)碼指令時,如果類沒有進行過初始化矛物,則需要先觸發(fā)其初始化茫死。生成這4條指令的最常見的Java代碼場景是:使用new關(guān)鍵字實例化對象的時候、讀取或設(shè)置一個類的靜態(tài)字段(被final修飾履羞、已在編譯器把結(jié)果放入常量池的靜態(tài)字段除外)的時候峦萎,以及調(diào)用一個類的靜態(tài)方法的時候。
  2. 使用java.lang.reflect包的方法對類進行反射調(diào)用的時候忆首,如果類沒有進行過初始化爱榔,則需要先觸發(fā)其初始化。
  3. 當(dāng)初始化一個類的時候糙及,如果發(fā)現(xiàn)其父類還沒有進行過初始化详幽,則需要先觸發(fā)其父類的初始化。
  4. 當(dāng)虛擬機啟動時丁鹉,用戶需要指定一個要執(zhí)行的主類(包含main()方法的那個類)妒潭,虛擬機會先初始化這個主類。
  5. 當(dāng)使用jdk1.7動態(tài)語言支持時揣钦,如果一個java.lang.invoke.MethodHandle實例最后的解析結(jié)果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄雳灾,并且這個方法句柄所對應(yīng)的類沒有進行初始化,則需要先出觸發(fā)其初始化

實現(xiàn)6:枚舉法冯凹,懶加載谎亩,線程安全,實現(xiàn)簡單

public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}

分析:這種實現(xiàn)方式還沒有被廣泛采用宇姚,但這是實現(xiàn)單例模式的最佳方法匈庭。它更簡潔,自動支持序列化機制浑劳,絕對防止多次實例化阱持。這種方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不僅能避免多線程同步問題魔熏,而且還自動支持序列化機制衷咽,防止反序列化重新創(chuàng)建新的對象,絕對防止多次實例化蒜绽。不過镶骗,由于 JDK1.5 之后才加入 enum 特性,用這種方式寫不免讓人感覺生疏躲雅,在實際工作中鼎姊,也很少用。不能通過 reflection attack 來調(diào)用私有構(gòu)造方法相赁。


總結(jié)

一般情況下相寇,不建議使用第 1 種和第 2 種懶漢方式,建議使用第 4 種餓漢方式噪生。只有在要明確實現(xiàn) lazy loading 效果時裆赵,才會使用第 5 種登記方式。如果涉及到反序列化創(chuàng)建對象時跺嗽,可以嘗試使用第 6 種枚舉方式战授。如果有其他特殊的需求,可以考慮使用第 3 種雙檢鎖方式桨嫁。

參考資料:


1植兰、阿里云大學(xué):https://edu.aliyun.com/lesson_471_4545?spm=5176.8764728.0.0.ZNKWmq#_4545

2、個人平時學(xué)習(xí)總結(jié)筆記璃吧。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末楣导,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子畜挨,更是在濱河造成了極大的恐慌筒繁,老刑警劉巖噩凹,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異毡咏,居然都是意外死亡驮宴,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門呕缭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來堵泽,“玉大人,你說我怎么就攤上這事恢总∮蓿” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵片仿,是天一觀的道長纹安。 經(jīng)常有香客問我,道長砂豌,這世上最難降的妖魔是什么钻蔑? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮奸鸯,結(jié)果婚禮上咪笑,老公的妹妹穿的比我還像新娘。我一直安慰自己娄涩,他們只是感情好窗怒,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蓄拣,像睡著了一般扬虚。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上球恤,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天辜昵,我揣著相機與錄音,去河邊找鬼咽斧。 笑死堪置,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的张惹。 我是一名探鬼主播舀锨,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼宛逗!你這毒婦竟也來了坎匿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎替蔬,沒想到半個月后告私,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡承桥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年德挣,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片快毛。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖番挺,靈堂內(nèi)的尸體忽然破棺而出唠帝,到底是詐尸還是另有隱情,我是刑警寧澤玄柏,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布襟衰,位于F島的核電站,受9級特大地震影響粪摘,放射性物質(zhì)發(fā)生泄漏瀑晒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一徘意、第九天 我趴在偏房一處隱蔽的房頂上張望苔悦。 院中可真熱鬧,春花似錦椎咧、人聲如沸玖详。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蟋座。三九已至,卻和暖如春脚牍,著一層夾襖步出監(jiān)牢的瞬間向臀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工诸狭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留券膀,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓驯遇,卻偏偏與公主長得像三娩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子妹懒,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

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