單例模式 - Singleton Pattern

簡介

什么是單例灯谣?為什么需要單例缆八?
單例模式的目的是設(shè)計出一個類曲掰,能提供全局唯一的對象疾捍。
舉個例子,程序中需要一個類用做管理配置項栏妖。這樣一個類顯然希望是全局只有一個乱豆,在任何地方都能獲取和使用,任何地方使用的對象也都是同一個底哥。這種情況就需要使用單例模式咙鞍!

簡單實現(xiàn)

舉例來說,我們要寫一個Singleton類的單例趾徽,首先我們先寫一個類:

public class Singleton{
}

要保證類是單例的,那么就不能隨意通過new操作符來隨意創(chuàng)建實例翰守。否則孵奶,每個new出來的對象都不是同一個對象,哪有單例可言蜡峰。為了禁止new操作符創(chuàng)建對象了袁,需要顯式地將構(gòu)造函數(shù)聲明為private

public class Singleton{
    private Singleton(){}
}

既然不能隨意創(chuàng)建,那么總得有一個方法能夠返回單例對象吧湿颅,我們命名為getInstance:

public class Singleton{
    private Singleton(){}
    public static Singleton getInstance(){
        //return a unique Singleton instance;
    }
}

重點:

  • getInstance必須是static方法载绿,因為只有static方法才可以直接通過Class調(diào)用
  • getInstance方法提供的必須是唯一實例

那么問題來了,getInstance方法如何能提供唯一實例呢油航?
第一種簡單的方法如下:

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

這種模式叫做餓漢模式崭庸,即一開始就創(chuàng)建一個對象,每次通過getInstance方法返回時谊囚,就返回這個對象怕享。很明顯,這個一定是單例的镰踏,全局只有一個Singeton對象函筋。
重點:

instance必須是static的。因為static方法中使用的只能是static對象奠伪。(這是因為static方法是可以通過class直接調(diào)用的跌帐,而非static成員必須有instance的時候才能使用。)

餓漢模式是如何保證線程安全的绊率?

JVM保證static成員只會被初始化一次谨敛。

缺點
“餓漢模式”有個缺點:

  1. 只要類被加載,對象就會被創(chuàng)建即舌,不管有沒有調(diào)用getInstance方法佣盒。如果單例對象是個大對象,沒有用到但又占著內(nèi)存空間顽聂,是比較浪費的肥惭。
  2. 如果單例對象的創(chuàng)建依賴于某些配置項盯仪,必須先配置,再創(chuàng)建對象蜜葱,那么餓汗模式是沒法使用的全景。

因此,衍生出另一種對應(yīng)的模式“懶漢模式”牵囤。

“懶漢模式”的特點是只有在使用的時候才創(chuàng)建對象爸黄。為了實現(xiàn)這種效果,很容易想到下面這種寫法:

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

關(guān)鍵點是揭鳞,先判斷instance對象有沒有已經(jīng)被創(chuàng)建過炕贵,如果等于null,說明沒有被創(chuàng)建過野崇,則創(chuàng)建一個称开,否則直接返回已有對象。
是不是感覺這樣寫沒毛才依妗鳖轰?No!No!No!這樣寫毛病很大!因為不是線程安全的扶镀。當(dāng)多個線程幾乎同時檢測if(instance == null)時蕴侣,都發(fā)現(xiàn)instance為null,這時都繼續(xù)往下走臭觉,從而創(chuàng)建了多個對象昆雀。如下圖所示:

非線程安全

如何寫出線程安全,并且性能良好的單例胧谈,一直是一個常見的面試題忆肾。

可能很多人可以很快給出以下方案:

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

這個方案是通過添加synchronized關(guān)鍵字來同步方法。
重點:

  • 使用synchronized來實現(xiàn)線程安全菱肖。在不考慮性能的情況下客冈,絕對簡單、有效稳强。

缺點:
這個方案很顯然能夠?qū)崿F(xiàn)線程安全场仲,但是性能堪憂。每次獲取單例對象都要進行同步退疫,有必要嗎渠缕?試想,當(dāng)一個對象創(chuàng)建以后褒繁,以后的所有操作都只是讀亦鳞,讀也同步的話,很顯然是影響效率的。

getInstance方法可以這樣寫嗎燕差?

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

可以遭笋!synchronized(Singleton.class)是個類鎖,在static方法上加synchronized關(guān)鍵字同樣是類鎖徒探,兩者是一樣的瓦呼。

引申

synchronized關(guān)鍵字 On the way...

簡單的加synchronized關(guān)鍵字的方案的問題主要在于鎖的粒度太大。只是簡單的像下面一樣減小鎖的粒度也是不行的:

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

這個方案又重新引入了線程安全問題测暗,synchronized關(guān)鍵字只是使并發(fā)創(chuàng)建對象編程了順序進行央串。這個方案直接pass。那么如何既能減小鎖的粒度碗啄,又能保證線程安全呢质和?

DCL

上面的方案減小的鎖的粒度,單不是線程安全稚字。我們可以在上面方案的基礎(chǔ)上繼續(xù)修改:

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

這個方案就是Double Check Lock(簡稱DCL)侦另。這個方案在上面方案的基礎(chǔ)上做了改進:synchronized同步塊里面能夠保證只創(chuàng)建一個對象。但是通過在synchronized的外面增加一層判斷尉共,就可以在對象一經(jīng)創(chuàng)建以后,不再進入synchronized同步塊弃锐。這種方案不僅減小了鎖的粒度袄友,保證了線程安全,性能方面也得到了大幅提升霹菊。

現(xiàn)在的方案是不是已經(jīng)很完美了剧蚣?
并!不旋廷!是鸠按!
為什么呢?

  1. 一個單例可以通過短的更多的代碼實現(xiàn)饶碘。
  2. DCL在極小的概率下目尖,創(chuàng)建的對象不可用!會報錯扎运!

我們首先講第二個問題瑟曲,DCL為何產(chǎn)生的對象有可能不可用。這是instance = new Singleton()不是原子操作豪治,而是可以大致分為以下三個步驟:

  1. Singleton對象分配內(nèi)存
  2. 初始化Singleton對象
  3. 將創(chuàng)建的對象引用賦值給instance

單線程情況下洞拨,instance = new Singleton會嚴(yán)格按照上述1,2,3步驟逐次執(zhí)行,并不會出錯负拟。但是在多線程情況下烦衣,由于在優(yōu)化時編譯器和CPU指令重排的存在,上述步驟執(zhí)行的次序有可能為1,3,2,這樣花吟,有可能出現(xiàn)如下的情況:

指令重排

我們期望的順序是1-2-3秸歧,但是由于指令重排,實際的順序可能是1-3-2示辈,這種情況下寥茫,線程1創(chuàng)建了一個單例對象,雖然instance已經(jīng)賦值了矾麻,但是對象還沒有初始化纱耻,線程2在第一次check instance是否為null的時候,得到對象已經(jīng)創(chuàng)建的錯誤信息险耀,就直接使用弄喘,顯然會出錯。這種情況出現(xiàn)概率極低甩牺,系統(tǒng)低負(fù)載的時候很難出現(xiàn)蘑志,但是高負(fù)載時,一旦出現(xiàn)問題贬派,就很難調(diào)查急但!

那么,有什么解決方案呢搞乏?可以使用volatile關(guān)鍵字波桩。volatile關(guān)鍵字修飾了instance后,有兩個作用:

  • 禁止對instance的操作重排指令
  • 并能保證多線程之間instance對象的及時可見性请敦。

如下:

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

重點

  • 使用volatile關(guān)鍵字禁止指令重排镐躲。
  • 這個事情僅在Java 1.5版后有用,1.5版之前用這個變量也有問題侍筛,因為老版本的Java的內(nèi)存模型是有缺陷的萤皂。

引申

詳解指令重排 On the way...
詳解volatile On the way...

還記不記得上面提到,單例可以有更短的代碼匣椰?對裆熙!使用靜態(tài)內(nèi)部類!

靜態(tài)內(nèi)部類

使用靜態(tài)內(nèi)部類實現(xiàn)單例模式的通用寫法如下:

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

重點:

  • 在靜態(tài)內(nèi)部類中創(chuàng)建單例對象窝爪。JVM保證static對象的創(chuàng)建是線程安全的弛车。
  • JVM還保證,一個類的任何static方法沒被調(diào)用的情況下蒲每,所有的static成員都不會被加載和初始化纷跛。這樣能保證單例對象只有在用的時候才會創(chuàng)建。
  • 依舊是懶漢模式邀杏,只有需要時才創(chuàng)建

內(nèi)部類可以是public的嗎贫奠?

不可以唬血!我們期望只有通過調(diào)用getInstance方法才能獲得單例對象,如果內(nèi)部類是public的唤崭,就破壞了封裝性拷恨。

instance成員可以是public的嗎?

不建議谢肾。成員對象用public修飾腕侄,同樣破壞了類的封裝性。不過非要這么寫芦疏,也沒辦法冕杠。

其實,還有更短的代碼K彳睢7衷ぁ!

枚舉

public enum Singleton{
    INSTANCE;
}

重點

  • 默認(rèn)枚舉實例的創(chuàng)建是線程安全的薪捍,所以不需要擔(dān)心線程安全的問題笼痹。
  • 《Effective Java》中推薦的模式

總結(jié)

單例模式是一個很常用的設(shè)計模式,也是一個在面試中常被問到的設(shè)計模式酪穿,更是一個很容易答錯的設(shè)計模式凳干。
單例模式雖然看起來簡單,但是設(shè)計的Java基礎(chǔ)知識非常多被济,如static修飾符纺座、synchronized修飾符、volatile修飾符溉潭、enum等。能正確地寫出說容易也行少欺,說難也行喳瓣。這個難易就在于個人的理解程度!很多人面試時遇到這個問題赞别,經(jīng)常丟三落四畏陕,少些一些關(guān)鍵修飾符,或者不清楚怎么寫仿滔。關(guān)鍵的點在于惠毁,不要死記硬背,一定要理解崎页!
同時鞠绰,更凸顯出java基礎(chǔ)知識的重要性!需不需要static飒焦,可不可以public蜈膨,這些都是有java基礎(chǔ)語法做為論據(jù)的屿笼。深刻理解了java語法,才能以不變應(yīng)萬變翁巍,才不怕忘記驴一!

如有問題,請留言灶壶,我會及時回復(fù)肝断,一起討論問題。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末驰凛,一起剝皮案震驚了整個濱河市胸懈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌洒嗤,老刑警劉巖箫荡,帶你破解...
    沈念sama閱讀 216,744評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異渔隶,居然都是意外死亡羔挡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評論 3 392
  • 文/潘曉璐 我一進店門间唉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绞灼,“玉大人,你說我怎么就攤上這事呈野〉桶” “怎么了?”我有些...
    開封第一講書人閱讀 163,105評論 0 353
  • 文/不壞的土叔 我叫張陵被冒,是天一觀的道長军掂。 經(jīng)常有香客問我,道長昨悼,這世上最難降的妖魔是什么蝗锥? 我笑而不...
    開封第一講書人閱讀 58,242評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮率触,結(jié)果婚禮上终议,老公的妹妹穿的比我還像新娘。我一直安慰自己葱蝗,他們只是感情好穴张,可當(dāng)我...
    茶點故事閱讀 67,269評論 6 389
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著两曼,像睡著了一般皂甘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上悼凑,一...
    開封第一講書人閱讀 51,215評論 1 299
  • 那天叮贩,我揣著相機與錄音击狮,去河邊找鬼。 笑死益老,一個胖子當(dāng)著我的面吹牛彪蓬,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播捺萌,決...
    沈念sama閱讀 40,096評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼档冬,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了桃纯?” 一聲冷哼從身側(cè)響起酷誓,我...
    開封第一講書人閱讀 38,939評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎态坦,沒想到半個月后盐数,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,354評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡伞梯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,573評論 2 333
  • 正文 我和宋清朗相戀三年玫氢,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谜诫。...
    茶點故事閱讀 39,745評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡漾峡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出喻旷,到底是詐尸還是另有隱情生逸,我是刑警寧澤,帶...
    沈念sama閱讀 35,448評論 5 344
  • 正文 年R本政府宣布且预,位于F島的核電站槽袄,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏锋谐。R本人自食惡果不足惜掰伸,卻給世界環(huán)境...
    茶點故事閱讀 41,048評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望怀估。 院中可真熱鬧,春花似錦合搅、人聲如沸多搀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽康铭。三九已至,卻和暖如春赌髓,著一層夾襖步出監(jiān)牢的瞬間从藤,已是汗流浹背催跪。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留夷野,地道東北人懊蒸。 一個月前我還...
    沈念sama閱讀 47,776評論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像悯搔,于是被迫代替她去往敵國和親骑丸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,652評論 2 354

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