單例模式

單例模式可能是代碼最少的模式了箭券,但是少不一定意味著簡(jiǎn)單细溅,想要用好厚掷、用對(duì)單例模式奕筐,還真得費(fèi)一番腦筋型雳。本文對(duì)Java中常見的單例模式寫法做了一個(gè)總結(jié)绢涡,如有錯(cuò)漏之處本谜,懇請(qǐng)讀者指正痕寓。

餓漢法

顧名思義蠢莺,餓漢法就是在第一次引用該類的時(shí)候就創(chuàng)建對(duì)象實(shí)例寒匙,而不管實(shí)際是否需要?jiǎng)?chuàng)建。代碼如下:

public class Singleton {   
    private static Singleton = new Singleton();
    private Singleton() {}
    public static getSignleton(){
        return singleton;
    }
}

這樣做的好處是編寫簡(jiǎn)單躏将,但是無法做到延遲創(chuàng)建對(duì)象锄弱。但是我們很多時(shí)候都希望對(duì)象可以盡可能地延遲加載,從而減小負(fù)載祸憋,所以就需要下面的懶漢法:

單線程寫法

這種寫法是最簡(jiǎn)單的会宪,由私有構(gòu)造器和一個(gè)公有靜態(tài)工廠方法構(gòu)成,在工廠方法中對(duì)singleton進(jìn)行null判斷蚯窥,如果是null就new一個(gè)出來掸鹅,最后返回singleton對(duì)象。這種方法可以實(shí)現(xiàn)延時(shí)加載拦赠,但是有一個(gè)致命弱點(diǎn):線程不安全巍沙。如果有兩條線程同時(shí)調(diào)用getSingleton()方法,就有很大可能導(dǎo)致重復(fù)創(chuàng)建對(duì)象荷鼠。

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

考慮線程安全的寫法

這種寫法考慮了線程安全句携,將對(duì)singleton的null判斷以及new的部分使用synchronized進(jìn)行加鎖。同時(shí)颊咬,對(duì)singleton對(duì)象使用volatile關(guān)鍵字進(jìn)行限制务甥,保證其對(duì)所有線程的可見性,并且禁止對(duì)其進(jìn)行指令重排序優(yōu)化喳篇。如此即可從語義上保證這種單例模式寫法是線程安全的敞临。注意,這里說的是語義上麸澜,實(shí)際使用中還是存在小坑的挺尿,會(huì)在后文寫到。

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

兼顧線程安全和效率的寫法

雖然上面這種寫法是可以正確運(yùn)行的,但是其效率低下编矾,還是無法實(shí)際應(yīng)用熟史。因?yàn)槊看握{(diào)用getSingleton()方法,都必須在synchronized這里進(jìn)行排隊(duì)窄俏,而真正遇到需要new的情況是非常少的蹂匹。所以,就誕生了第三種寫法:

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

這種寫法被稱為“雙重檢查鎖”凹蜈,顧名思義限寞,就是在getSingleton()方法中,進(jìn)行兩次null檢查仰坦÷闹玻看似多此一舉,但實(shí)際上卻極大提升了并發(fā)度悄晃,進(jìn)而提升了性能玫霎。為什么可以提高并發(fā)度呢?就像上文說的妈橄,在單例中new的情況非常少庶近,絕大多數(shù)都是可以并行的讀操作。因此在加鎖前多進(jìn)行一次null檢查就可以減少絕大多數(shù)的加鎖操作眷蚓,執(zhí)行效率提高的目的也就達(dá)到了拦盹。

那么,這種寫法是不是絕對(duì)安全呢溪椎?前面說了,從語義角度來看恬口,并沒有什么問題校读。但是其實(shí)還是有坑。說這個(gè)坑之前我們要先來看看volatile這個(gè)關(guān)鍵字祖能。其實(shí)這個(gè)關(guān)鍵字有兩層語義歉秫。第一層語義相信大家都比較熟悉,就是可見性养铸⊙丬剑可見性指的是在一個(gè)線程中對(duì)該變量的修改會(huì)馬上由工作內(nèi)存(Work Memory)寫回主內(nèi)存(Main Memory),所以會(huì)馬上反應(yīng)在其它線程的讀取操作中钞螟。順便一提兔甘,工作內(nèi)存和主內(nèi)存可以近似理解為實(shí)際電腦中的高速緩存和主存,工作內(nèi)存是線程獨(dú)享的鳞滨,主存是線程共享的洞焙。volatile的第二層語義是禁止指令重排序優(yōu)化。大家知道我們寫的代碼(尤其是多線程代碼),由于編譯器優(yōu)化澡匪,在實(shí)際執(zhí)行的時(shí)候可能與我們編寫的順序不同熔任。編譯器只保證程序執(zhí)行結(jié)果與源代碼相同,卻不保證實(shí)際指令的順序與源代碼相同唁情。這在單線程看起來沒什么問題疑苔,然而一旦引入多線程,這種亂序就可能導(dǎo)致嚴(yán)重問題甸鸟。volatile關(guān)鍵字就可以從語義上解決這個(gè)問題惦费。

注意,前面反復(fù)提到“從語義上講是沒有問題的”哀墓,但是很不幸趁餐,禁止指令重排優(yōu)化這條語義直到j(luò)dk1.5以后才能正確工作。此前的JDK中即使將變量聲明為volatile也無法完全避免重排序所導(dǎo)致的問題篮绰。所以后雷,在jdk1.5版本前,雙重檢查鎖形式的單例模式是無法保證線程安全的吠各。

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

那么臀突,有沒有一種延時(shí)加載,并且能保證線程安全的簡(jiǎn)單寫法呢贾漏?我們可以把Singleton實(shí)例放到一個(gè)靜態(tài)內(nèi)部類中候学,這樣就避免了靜態(tài)實(shí)例在Singleton類加載的時(shí)候就創(chuàng)建對(duì)象,并且由于靜態(tài)內(nèi)部類只會(huì)被加載一次纵散,所以這種寫法也是線程安全的:

public class Singleton {
    private static class Holder {
        private static Singleton singleton = new Singleton();
    }
     
    private Singleton(){}
         
    public static Singleton getSingleton(){
        return Holder.singleton;
    }
}

但是梳码,上面提到的所有實(shí)現(xiàn)方式都有兩個(gè)共同的缺點(diǎn):

  • 都需要額外的工作(Serializable、transient伍掀、readResolve())來實(shí)現(xiàn)序列化掰茶,否則每次反序列化一個(gè)序列化的對(duì)象實(shí)例時(shí)都會(huì)創(chuàng)建一個(gè)新的實(shí)例。

  • 可能會(huì)有人使用反射強(qiáng)行調(diào)用我們的私有構(gòu)造器(如果要避免這種情況蜜笤,可以修改構(gòu)造器濒蒋,讓它在創(chuàng)建第二個(gè)實(shí)例的時(shí)候拋異常)。

枚舉寫法

當(dāng)然把兔,還有一種更加優(yōu)雅的方法來實(shí)現(xiàn)單例模式沪伙,那就是枚舉寫法:

public enum Singleton {
    INSTANCE;
    private String name;
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
}

使用枚舉除了線程安全和防止反射強(qiáng)行調(diào)用構(gòu)造器之外,還提供了自動(dòng)序列化機(jī)制县好,防止反序列化的時(shí)候創(chuàng)建新的對(duì)象围橡。因此,Effective Java推薦盡可能地使用枚舉來實(shí)現(xiàn)單例聘惦。

總結(jié)

這篇文章發(fā)出去以后得到許多反饋某饰,這讓我受寵若驚儒恋,覺得應(yīng)該再寫一點(diǎn)小結(jié)。代碼沒有一勞永逸的寫法黔漂,只有在特定條件下最合適的寫法诫尽。在不同的平臺(tái)、不同的開發(fā)環(huán)境(尤其是jdk版本)下炬守,自然有不同的最優(yōu)解(或者說較優(yōu)解)牧嫉。
比如枚舉,雖然Effective Java中推薦使用减途,但是在Android平臺(tái)上卻是不被推薦的酣藻。在這篇Android Training中明確指出:

Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

再比如雙重檢查鎖法,不能在jdk1.5之前使用鳍置,而在Android平臺(tái)上使用就比較放心了(一般Android都是jdk1.6以上了辽剧,不僅修正了volatile的語義問題,還加入了不少鎖優(yōu)化税产,使得多線程同步的開銷降低不少)怕轿。

最后,不管采取何種方案辟拷,請(qǐng)時(shí)刻牢記單例的三大要點(diǎn):

  • 線程安全
  • 延遲加載
  • 序列化與反序列化安全
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末撞羽,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子衫冻,更是在濱河造成了極大的恐慌诀紊,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件隅俘,死亡現(xiàn)場(chǎng)離奇詭異邻奠,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)为居,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門惕澎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人颜骤,你說我怎么就攤上這事〉仿保” “怎么了忍抽?”我有些...
    開封第一講書人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)董朝。 經(jīng)常有香客問我鸠项,道長(zhǎng),這世上最難降的妖魔是什么子姜? 我笑而不...
    開封第一講書人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任祟绊,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘牧抽。我一直安慰自己嘉熊,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開白布扬舒。 她就那樣靜靜地躺著阐肤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪讲坎。 梳的紋絲不亂的頭發(fā)上孕惜,一...
    開封第一講書人閱讀 49,760評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音晨炕,去河邊找鬼衫画。 笑死,一個(gè)胖子當(dāng)著我的面吹牛瓮栗,可吹牛的內(nèi)容都是我干的削罩。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼遵馆,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼鲸郊!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起货邓,我...
    開封第一講書人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤秆撮,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后换况,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體职辨,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年戈二,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了舒裤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡觉吭,死狀恐怖腾供,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情鲜滩,我是刑警寧澤伴鳖,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站徙硅,受9級(jí)特大地震影響榜聂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜嗓蘑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一须肆、第九天 我趴在偏房一處隱蔽的房頂上張望匿乃。 院中可真熱鬧,春花似錦豌汇、人聲如沸幢炸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽阳懂。三九已至,卻和暖如春柜思,著一層夾襖步出監(jiān)牢的瞬間岩调,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工赡盘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留号枕,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓陨享,卻偏偏與公主長(zhǎng)得像葱淳,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子抛姑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

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

  • 前言 本文主要參考 那些年赞厕,我們一起寫過的“單例模式”。 何為單例模式定硝? 顧名思義皿桑,單例模式就是保證一個(gè)類僅有一個(gè)...
    tandeneck閱讀 2,488評(píng)論 1 8
  • 1.單例模式概述 (1)引言 單例模式是應(yīng)用最廣的模式之一,也是23種設(shè)計(jì)模式中最基本的一個(gè)蔬啡。本文旨在總結(jié)通過Ja...
    曹豐斌閱讀 2,888評(píng)論 6 47
  • 單例模式(SingletonPattern)一般被認(rèn)為是最簡(jiǎn)單诲侮、最易理解的設(shè)計(jì)模式,也因?yàn)樗暮?jiǎn)潔易懂箱蟆,是項(xiàng)目中最...
    成熱了閱讀 4,231評(píng)論 4 34
  • 一.什么是單例模式 單例模式的定義:確保一個(gè)類只有一個(gè)實(shí)例沟绪,并提供一個(gè)訪問他的全局訪問點(diǎn)。單例模式是幾個(gè)設(shè)計(jì)模式中...
    Geeks_Liu閱讀 2,216評(píng)論 0 10
  • 說話堵人嘴,老了及后悔辈毯,因?yàn)閭烁星閱峋眯牛恳惠呑影缘涝捳f多了的人,老了一定會(huì)活得很孤苦漓摩。為何養(yǎng)兒十幾個(gè)老了自...
    逍遙真龍哥哥閱讀 584評(píng)論 0 4