Java中的單例模式與DoubleCheck---易錯(cuò)點(diǎn)

前言

??在GoF的23種設(shè)計(jì)模式中崎场,單例模式是比較簡(jiǎn)單的一種佩耳。然而,有時(shí)候越是簡(jiǎn)單的東西越容易出現(xiàn)問(wèn)題照雁。下面就單例設(shè)計(jì)模式詳細(xì)的探討一下蚕愤。

??所謂單例模式答恶,簡(jiǎn)單來(lái)說(shuō)饺蚊,就是在整個(gè)應(yīng)用中保證類只有一個(gè)實(shí)例存在。這個(gè)類的實(shí)例只提供了一個(gè)全局變量悬嗓,用處相當(dāng)廣泛污呼,比如保存全局?jǐn)?shù)據(jù),實(shí)現(xiàn)全局性的操作等包竹。

分析

??1. 最簡(jiǎn)單的實(shí)現(xiàn) —— 餓漢式

??首先燕酷,能夠想到的最簡(jiǎn)單的實(shí)現(xiàn)是,把類的構(gòu)造函數(shù)寫成private的周瞎,從而保證別的類不能實(shí)例化此類苗缩,然后在類中提供一個(gè)靜態(tài)的實(shí)例并能夠返回給使用者。這樣声诸,使用者就可以通過(guò)這個(gè)引用使用到這個(gè)類的實(shí)例了酱讶。

 
public class SingletonClass { 

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

外部使用者如果需要使用SingletonClass的實(shí)例,只能通過(guò)getInstance()方法彼乌,并且它的構(gòu)造方法是private的泻肯,這樣就保證了只能有一個(gè)對(duì)象存在渊迁。


??2. 性能優(yōu)化 —— lazy loaded 懶漢式

??上面的代碼雖然簡(jiǎn)單,但是有一個(gè)問(wèn)題——無(wú)論這個(gè)類是否被使用灶挟,都會(huì)創(chuàng)建一個(gè)instance對(duì)象琉朽。如果這個(gè)創(chuàng)建過(guò)程很耗時(shí),比如需要連接10000次jdbc實(shí)例連接或者10000多個(gè)模版實(shí)例稚铣,并且這個(gè)類還并不一定會(huì)被使用箱叁,那么這個(gè)創(chuàng)建過(guò)程就是無(wú)用的。

??為了解決這個(gè)問(wèn)題惕医,我們想到了新的解決方案:


public class SingletonClass { 

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

??代碼的變化有1處——把instance初始化為null蝌蹂,直到第一次使用的時(shí)候通過(guò)判斷是否為null來(lái)創(chuàng)建對(duì)象。

??我們來(lái)想象一下這個(gè)過(guò)程曹锨。要使用SingletonClass孤个,調(diào)用getInstance()方法。第一次的時(shí)候發(fā)現(xiàn)instance是null沛简,然后就新建一個(gè)對(duì)象齐鲤,返回出去;第二次再使用的時(shí)候椒楣,因?yàn)檫@個(gè)instance是static的给郊,所以已經(jīng)不是null了,因此不會(huì)再創(chuàng)建對(duì)象捧灰,直接將其返回淆九。

這個(gè)過(guò)程就成為lazy loaded,也就是延遲加載——直到使用的時(shí)候才進(jìn)行加載毛俏。


??3. 同步

??上面的代碼很清楚炭庙,也很簡(jiǎn)單。然而就像那句名言:“80%的錯(cuò)誤都是由20%代碼優(yōu)化引起的”煌寇。單線程下焕蹄,這段代碼沒(méi)有什么問(wèn)題,可是如果是多線程阀溶,麻煩就來(lái)了腻脏。我們來(lái)分析一下:
??線程1希望使用SingletonClass,調(diào)用getInstance()方法银锻。因?yàn)槭堑谝淮握{(diào)用永品,1就發(fā)現(xiàn)instance是null的,于是它開(kāi)始創(chuàng)建實(shí)例击纬,就在這個(gè)時(shí)候鼎姐,CPU發(fā)生時(shí)間片切換(或者被搶奪執(zhí)行),線程2開(kāi)始執(zhí)行,它要使用SingletonClass症见,調(diào)用getInstance()方法喂走,同樣檢測(cè)到instance是null——注意,這是在1檢測(cè)完之后切換的谋作,也就是說(shuō)1并沒(méi)有來(lái)得及創(chuàng)建對(duì)象——因此2開(kāi)始創(chuàng)建芋肠。2創(chuàng)建完成后,cpu切換到1繼續(xù)執(zhí)行遵蚜,因?yàn)樗呀?jīng)檢測(cè)完了帖池,所以1不會(huì)再檢測(cè)一遍,它會(huì)直接創(chuàng)建對(duì)象吭净。這樣睡汹,線程1和2各自擁有一個(gè)SingletonClass的對(duì)象——單例失敗寂殉!

??解決的方法也很簡(jiǎn)單囚巴,那就是加鎖:

public class SingletonClass { 

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


??4. 又是性能問(wèn)題

??上面的代碼又是很清楚很簡(jiǎn)單的,然而友扰,簡(jiǎn)單的東西往往不夠理想彤叉。理想的東西往往不夠簡(jiǎn)單,這就是生活村怪。這段代碼毫無(wú)疑問(wèn)存在性能的問(wèn)題——synchronized修飾的同步塊可是要比一般的代碼段慢上幾倍的秽浇!如果存在很多次getInstance()的調(diào)用,那性能問(wèn)題就不得不考慮了甚负!

??讓我們來(lái)分析一下柬焕,究竟是整個(gè)方法都必須加鎖,還是僅僅其中某一句加鎖就足夠了梭域?我們?yōu)槭裁匆渔i呢斑举?分析一下出現(xiàn)lazy loaded的那種情形的原因。原因就是檢測(cè)null的操作和創(chuàng)建對(duì)象的操作分離了碰辅。如果這兩個(gè)操作能夠原子地進(jìn)行懂昂,那么單例就已經(jīng)保證了介时。于是没宾,我們開(kāi)始修改代碼:

public class SingletonClass { 

  private static SingletonClass instance = null; 

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

  private SingletonClass() { 

  } 

}

還有問(wèn)題嗎?首先判斷instance是不是為null沸柔,如果為null循衰,加鎖初始化;如果不為null褐澎,直接返回instance会钝。

這就是double-checked locking設(shè)計(jì)實(shí)現(xiàn)單例模式。但是還有問(wèn)題。迁酸。先鱼。。奸鬓。焙畔。

??5. JMM中

??在Java虛擬機(jī)規(guī)范中試圖定義一種Java內(nèi)存模型(Java Memory Model,JMM)來(lái)屏蔽各個(gè)硬件平臺(tái)和操作系統(tǒng)的內(nèi)存訪問(wèn)差異串远,以實(shí)現(xiàn)讓Java程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪問(wèn)效果宏多。那么Java內(nèi)存模型規(guī)定了哪些東西呢,它定義了程序中變量的訪問(wèn)規(guī)則澡罚,往大一點(diǎn)說(shuō)是定義了程序執(zhí)行的次序伸但。注意,為了獲得較好的執(zhí)行性能留搔,Java內(nèi)存模型并沒(méi)有限制執(zhí)行引擎使用處理器的寄存器或者高速緩存來(lái)提升指令執(zhí)行速度更胖,也沒(méi)有限制編譯器對(duì)指令進(jìn)行重排序。也就是說(shuō)隔显,在java內(nèi)存模型中函喉,也會(huì)存在緩存一致性問(wèn)題和指令重排序的問(wèn)題。
??下面來(lái)想一下荣月,創(chuàng)建一個(gè)變量需要哪些步驟呢管呵?一個(gè)是申請(qǐng)一塊內(nèi)存,調(diào)用構(gòu)造方法進(jìn)行初始化操作哺窄,另一個(gè)是分配一個(gè)指針指向這塊內(nèi)存捐下。這兩個(gè)操作誰(shuí)在前誰(shuí)在后呢?JMM規(guī)范并沒(méi)有規(guī)定萌业。(可能重排序)那么就存在這么一種情況坷襟,JVM是先開(kāi)辟出一塊內(nèi)存,然后把指針指向這塊內(nèi)存生年,最后調(diào)用構(gòu)造方法進(jìn)行初始化婴程。
??線程1開(kāi)始創(chuàng)建SingletonClass的實(shí)例,此時(shí)線程2調(diào)用了getInstance()方法抱婉,首先判斷instance是否為null档叔。按照我們上面所說(shuō)的內(nèi)存模型,1已經(jīng)把instance指向了那塊內(nèi)存蒸绩,只是還沒(méi)有調(diào)用構(gòu)造方法衙四,因此2檢測(cè)到instance不為null,于是直接把instance返回了——問(wèn)題出現(xiàn)了患亿,盡管instance不為null扎瓶,但它并沒(méi)有構(gòu)造完成,就像一套房子已經(jīng)給了你鑰匙祭阀,但你并不能住進(jìn)去,因?yàn)槔锩孢€是毛坯房挑格。此時(shí),如果2在1將instance構(gòu)造完成之前就是用了這個(gè)實(shí)例沾歪,程序就會(huì)出現(xiàn)錯(cuò)誤了恕齐!

??5. 最終解決方案

??在JDK 5之后,Java使用了新的內(nèi)存模型瞬逊。volatile關(guān)鍵字有了明確的語(yǔ)義——在JDK1.5之前显歧,volatile是個(gè)關(guān)鍵字,但是并沒(méi)有明確的規(guī)定其用途——被volatile修飾的寫變量不能和之前的讀寫代碼調(diào)整确镊,讀變量不能和之后的讀寫代碼調(diào)整士骤!因此,只要我們簡(jiǎn)單的把instance加上volatile關(guān)鍵字就可以了蕾域。

public class SingletonClass { 

  private volatile static SingletonClass instance = null; 

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

  private SingletonClass() { 

  } 
    
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末拷肌,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子旨巷,更是在濱河造成了極大的恐慌巨缘,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件采呐,死亡現(xiàn)場(chǎng)離奇詭異若锁,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)斧吐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門又固,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人煤率,你說(shuō)我怎么就攤上這事仰冠。” “怎么了蝶糯?”我有些...
    開(kāi)封第一講書人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵洋只,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我昼捍,道長(zhǎng)识虚,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任端三,我火速辦了婚禮舷礼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘郊闯。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布团赁。 她就那樣靜靜地躺著育拨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪欢摄。 梳的紋絲不亂的頭發(fā)上熬丧,一...
    開(kāi)封第一講書人閱讀 51,541評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音怀挠,去河邊找鬼析蝴。 笑死,一個(gè)胖子當(dāng)著我的面吹牛绿淋,可吹牛的內(nèi)容都是我干的闷畸。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼吞滞,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼佑菩!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起裁赠,我...
    開(kāi)封第一講書人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤殿漠,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后佩捞,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體绞幌,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年一忱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了啊奄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡掀潮,死狀恐怖菇夸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情仪吧,我是刑警寧澤庄新,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站薯鼠,受9級(jí)特大地震影響择诈,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜出皇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一羞芍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧郊艘,春花似錦荷科、人聲如沸唯咬。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)胆胰。三九已至,卻和暖如春刻获,著一層夾襖步出監(jiān)牢的瞬間蜀涨,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工蝎毡, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留厚柳,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓沐兵,卻偏偏與公主長(zhǎng)得像别垮,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子痒筒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

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