Java設(shè)計(jì)模式之-單例模式(Singleton)

先說(shuō)一下我自己對(duì)單例模式的理解:

單例模式:在整個(gè)程序運(yùn)行周期內(nèi)本谜,某個(gè)類被設(shè)計(jì)為其所有實(shí)例都?xì)w屬于一個(gè)副本,以保證含義上的唯一性和行為上的總控性。這種類的設(shè)計(jì)方式被稱為單例模式。

如果某個(gè)類從現(xiàn)實(shí)世界角度來(lái)看丛肮,確實(shí)應(yīng)該只存在一個(gè)實(shí)例副本,或者該類的行為是作為整個(gè)系統(tǒng)中某個(gè)功能的總控統(tǒng)籌魄缚,將它通過(guò)單例模式來(lái)實(shí)現(xiàn),能夠提供良好的可維護(hù)性和準(zhǔn)確性焚廊,也更節(jié)省占用的內(nèi)存和新生成實(shí)例的開(kāi)銷冶匹。例如管理一個(gè)JDBC連接的類,或者一個(gè)Canvas中的畫筆類等咆瘟。

為了實(shí)現(xiàn)單例嚼隘,首先不能讓類被隨意地實(shí)例化。我們可以通過(guò)創(chuàng)建private構(gòu)造函數(shù)來(lái)屏蔽new關(guān)鍵字的調(diào)用袒餐。

在網(wǎng)上隨便搜一下飞蛹,能夠看到五花八門的單例模式說(shuō)明谤狡,各種名詞層出不窮,餓漢卧檐、懶漢墓懂、飽漢等等...但在這篇文章中我想層層遞進(jìn)地說(shuō)一下各種實(shí)現(xiàn)方式的進(jìn)化關(guān)系。

基本的單例

在我剛開(kāi)始寫代碼的時(shí)候霉囚,曾經(jīng)也遇到過(guò)需要只存在一個(gè)實(shí)例的場(chǎng)景捕仔。當(dāng)時(shí)還很懵懂,就寫出了如下的代碼:

// “懶漢” - 延遲初始化盈罐,非線程安全
// “懶”表現(xiàn)為: instance要等到真正使用時(shí)(getInstance)才會(huì)被實(shí)例化
public class Singleton1 {  
     private static Singleton1 instance;  
     private Singleton1 (){}   
     public static Singleton1 getInstance() {  
         if (instance == null) {  
             instance = new Singleton1();  
         }  
         return instance;  
     }  
}  

以上代碼是非線程安全的榜跌。
試想當(dāng)在多線程場(chǎng)景中,許多線程幾乎同時(shí)調(diào)用getInstance方法盅粪,并判斷instance == null為true,這個(gè)時(shí)候便會(huì)有多個(gè)現(xiàn)成去執(zhí)行實(shí)例化代碼钓葫。這樣一來(lái)便無(wú)法保證類的單例了。

而另外一種比較基本的單例寫法是:

//“餓漢”(其實(shí)我想稱它為“勤男”) - 立即初始化票顾,線程安全
//"餓"體現(xiàn)在: 餓漢很餓础浮,希望盡早吃到內(nèi)存
//"勤"體現(xiàn)在:人家一上來(lái)就把內(nèi)存區(qū)開(kāi)好了,相比于上面的“懶漢”库物,當(dāng)然是“勤”了
public class Singleton2 {  
    private static Singleton2 instance = new Singleton2();  
    private Singleton2 (){}
    public static Singleton2 getInstance() {  
        return instance;  
    }  
}  

這種寫法使用Java的語(yǔ)法糖霸旗,能夠滿足多線程的并發(fā)需求。但是從延遲初始化的角度來(lái)說(shuō)戚揭,是有欠缺的诱告。尤其是當(dāng)Singleton是一個(gè)比較復(fù)雜的類,無(wú)法簡(jiǎn)單地通過(guò)new關(guān)鍵字進(jìn)行實(shí)例化民晒,或者需要獲得某些參數(shù)才能完成實(shí)例化時(shí)精居,延遲初始化就成了我們的必選項(xiàng)。

從上面我們可以看到潜必,兩種基本的單例實(shí)現(xiàn)方式靴姿,都存在各自的缺點(diǎn)。根據(jù)上面的分析磁滚,其實(shí)我們想要的是一種既能延遲初始化佛吓,又保證了多線程安全的單例實(shí)現(xiàn)模式。


延遲初始化+線程安全的單例

為了要保證二者兼得垂攘,我們?cè)诘谝环N代碼的基礎(chǔ)上维雇,增加能夠保證線程安全的代碼即可。為此晒他,我們引入synchronized關(guān)鍵字吱型。

synchronized關(guān)鍵字

synchronized關(guān)鍵字,可用來(lái)給對(duì)象和方法或者代碼塊加鎖陨仅,當(dāng)它鎖定一個(gè)方法或者一個(gè)代碼塊的時(shí)候津滞,同一時(shí)刻最多只有一個(gè)線程執(zhí)行這段代碼铝侵。另一個(gè)線程必須等待當(dāng)前線程執(zhí)行完這個(gè)代碼塊以后才能執(zhí)行該代碼塊。

由于是說(shuō)設(shè)計(jì)模式的文章触徐,這里就不展開(kāi)討論synchronized了咪鲜。按照上面的描述,如果沒(méi)研究過(guò)它的朋友應(yīng)該也能基本了解它的作用锌介。我們使用它來(lái)實(shí)現(xiàn)一個(gè)滿足兩個(gè)條件的單例:

//"懶漢"變種
//用 synchronized 修飾 getInstance方法
public class Singleton3 {  
     private static Singleton3 instance;  
     private Singleton3 (){}
     public static synchronized Singleton3 getInstance() {  
         if (instance == null) {  
             instance = new Singleton3();  
         }  
         return instance;  
     }  
}  

這種寫法滿足了線程安全嗜诀,但是安全過(guò)頭了。在多線程場(chǎng)景中孔祸,同一時(shí)刻只能有一個(gè)線程訪問(wèn)getInstance方法隆敢,換言之就是多線程在這個(gè)方法上變成了單線程。大家可以想到崔慧,即使后續(xù)的線程中instance已經(jīng)不為null了拂蝎,但還是要等待前序線程執(zhí)行完該方法。這無(wú)疑是對(duì)效率的一大阻礙惶室。

Double Check Lock (DCL)

為了提升多線程效率温自,我們將synchronized換了個(gè)位置。但是為了確保單例皇钞,我們又在synchronized內(nèi)部增加了一次if判斷悼泌,這樣便有了兩次null檢查,即DCL:

//"懶漢"變種
//用 synchronized 修飾 getInstance方法
public class Singleton4 {  
     private static Singleton4 instance;  
     private Singleton4 (){}
     public static synchronized Singleton4 getInstance() {  
         if (instance == null) {  
             synchronized(Singleton4.class){
                 if(instance == null) {
                     instance = new Singleton4();
                 }  
             }
         }  
         return instance;  
     }  
}  

由于synchronized作用域已經(jīng)從一個(gè)方法縮小到了一段代碼塊夹界,多個(gè)線程可以同時(shí)訪問(wèn)第一個(gè)if判斷馆里,如果instance不為null便可以直接返回,不用等待可柿。這種寫法雖然奇怪鸠踪,但是看起來(lái)確實(shí)實(shí)現(xiàn)了延時(shí)初始化和線程安全,并且提升了多線程的效率复斥。

但是實(shí)際上营密,這種方式并沒(méi)有保證完全的線程安全,罪魁禍?zhǔn)妆闶?strong>指令重排序目锭。

指令重排序

instance = new Singleton4();這行代碼评汰,可以被編譯器編成以下三行指令:

  • rawMemory = allocateMemory(); //分配內(nèi)存
  • preparedMemory = initMemory(rawMemory); //初始化內(nèi)存
  • instance = preparedMemory; //內(nèi)存與字段綁定

試問(wèn)如果編譯器將結(jié)果優(yōu)化為以下序列,將后兩個(gè)指令調(diào)換順序痢虹,多線程情況下會(huì)出現(xiàn)什么結(jié)果键俱?

  • 分配內(nèi)存
  • 內(nèi)存與字段綁定
  • 初始化內(nèi)存

有的線程可能在剛進(jìn)入該方法時(shí),剛好上述指令執(zhí)行到了第二步世分,因此instance不為null。但實(shí)際上此時(shí)instance還沒(méi)有被初始化完成缀辩,線程拿到的是一個(gè)殘缺的非null實(shí)例臭埋。

volatile關(guān)鍵字

volatile關(guān)鍵字保證了對(duì)應(yīng)的字段能夠含有一下特性:

  1. 保證了不同線程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見(jiàn)性踪央,即一個(gè)線程修改變量值,這新值對(duì)其他線程來(lái)說(shuō)是立即可見(jiàn)的瓢阴。
  2. 禁止對(duì)操作該變量的指令重排序畅蹂。

具體關(guān)于volatile關(guān)鍵字的說(shuō)明可以參見(jiàn)網(wǎng)上其他文章,這里也不再贅述荣恐,大家只要先了解上述兩個(gè)特性液斜。下面我們來(lái)利用volatile對(duì)我們的單例模式進(jìn)行進(jìn)一步的加工:

//"懶漢"變種
//用 volatile 修飾 instance
public class Singleton5 {  
     private static volatile Singleton5 instance = null;  
     private Singleton5 (){}
     public static synchronized Singleton5 getInstance() {  
         if (instance == null) {  
             synchronized(Singleton5.class){
                 if(instance == null) {
                     instance = new Singleton4();
                 }  
             }
         }  
         return instance;  
     }  
}  

通過(guò)使用volatile修飾instance,保證了實(shí)例化時(shí)指令的正確順序叠穆,也確保了多線程安全少漆。這種寫法基本上實(shí)現(xiàn)了一個(gè)單例的基本要求。


套路的單例

Holder模式

除了上面組合使用synchronized和volatile進(jìn)行多線程安全保護(hù)外硼被,我們還可以按照Holder方式將基本的實(shí)例中第二種“勤漢”模式進(jìn)行修改示损,從而再實(shí)現(xiàn)一套即延遲初始化,又保證線程安全的代碼:

//Holder模式
//引入靜態(tài)類嚷硫,該類在首次實(shí)際使用時(shí)進(jìn)行內(nèi)存分配检访,即return SingletonHolder.INSTANCE時(shí)
public class Singleton6 {  
     private static class SingletonHolder {  
         private static final Singleton6 INSTANCE = new Singleton6();  
     }  
     private Singleton6 (){}
     public static final Singleton6 getInstance() {  
         return SingletonHolder.INSTANCE;  
     }  
}  

枚舉模式(Since jdk1.5)

由于枚舉的特性,它實(shí)際上是一個(gè)天然的單例仔掸,確保了實(shí)例副本的唯一性:

public enum Singleton7{
    INSTANCE;
}

枚舉型的單例能夠支持線程安全脆贵,且能夠?qū)崿F(xiàn)延遲加載,并且還能防止反序列化問(wèn)題和反射攻擊問(wèn)題起暮,不愧為Effective Java中提出的完美的單例解決方案卖氨。


反序列化問(wèn)題和反射問(wèn)題

這一塊內(nèi)容完全是為了對(duì)單例模式進(jìn)行補(bǔ)充,如果只是為了了解設(shè)計(jì)模式的話鞋怀,可以不再往下閱讀了双泪。

由于單例模式最重要的一點(diǎn)就是保證該類的實(shí)例副本的唯一性,而如果這個(gè)類支持序列化密似,那么在反序列化的時(shí)候我便可以產(chǎn)生多個(gè)實(shí)例焙矛,即反序列化是一個(gè)隱藏很深的構(gòu)造函數(shù)。如果不對(duì)這種情況進(jìn)行封鎖残腌,勢(shì)必會(huì)破壞單例村斟。

private Singleton readResolve(){
    return getInstance();
}

通過(guò)實(shí)現(xiàn)readResolve方法,在反序列化時(shí)抛猫,跳過(guò)默認(rèn)邏輯蟆盹,而使用已經(jīng)寫好的getInstance方法,能夠規(guī)避這種問(wèn)題闺金。

而反射問(wèn)題則是利用Java的反射機(jī)制逾滥,調(diào)用到private訪問(wèn)權(quán)限的構(gòu)造函數(shù),從而生成了多個(gè)實(shí)例败匹。針對(duì)這個(gè)問(wèn)題寨昙,目前可以看到的是枚舉模式能夠完美地進(jìn)行處理讥巡。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市舔哪,隨后出現(xiàn)的幾起案子欢顷,更是在濱河造成了極大的恐慌,老刑警劉巖捉蚤,帶你破解...
    沈念sama閱讀 212,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件抬驴,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡缆巧,警方通過(guò)查閱死者的電腦和手機(jī)布持,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)盅蝗,“玉大人鳖链,你說(shuō)我怎么就攤上這事《漳” “怎么了芙委?”我有些...
    開(kāi)封第一講書人閱讀 158,369評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)狂秦。 經(jīng)常有香客問(wèn)我灌侣,道長(zhǎng),這世上最難降的妖魔是什么裂问? 我笑而不...
    開(kāi)封第一講書人閱讀 56,799評(píng)論 1 285
  • 正文 為了忘掉前任侧啼,我火速辦了婚禮,結(jié)果婚禮上堪簿,老公的妹妹穿的比我還像新娘痊乾。我一直安慰自己,他們只是感情好椭更,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,910評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布哪审。 她就那樣靜靜地躺著,像睡著了一般虑瀑。 火紅的嫁衣襯著肌膚如雪湿滓。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 50,096評(píng)論 1 291
  • 那天舌狗,我揣著相機(jī)與錄音叽奥,去河邊找鬼。 笑死痛侍,一個(gè)胖子當(dāng)著我的面吹牛朝氓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 39,159評(píng)論 3 411
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼膀篮,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼嘹狞!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起誓竿,我...
    開(kāi)封第一講書人閱讀 37,917評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎谈截,沒(méi)想到半個(gè)月后筷屡,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,360評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡簸喂,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,673評(píng)論 2 327
  • 正文 我和宋清朗相戀三年毙死,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片喻鳄。...
    茶點(diǎn)故事閱讀 38,814評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡扼倘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出除呵,到底是詐尸還是另有隱情再菊,我是刑警寧澤,帶...
    沈念sama閱讀 34,509評(píng)論 4 334
  • 正文 年R本政府宣布颜曾,位于F島的核電站纠拔,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏泛豪。R本人自食惡果不足惜稠诲,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,156評(píng)論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望诡曙。 院中可真熱鬧臀叙,春花似錦、人聲如沸价卤。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)荠雕。三九已至稳其,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間炸卑,已是汗流浹背既鞠。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,123評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留盖文,地道東北人嘱蛋。 一個(gè)月前我還...
    沈念sama閱讀 46,641評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親洒敏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子龄恋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,728評(píng)論 2 351

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