單例類在Java開發(fā)者中非常常用,但是它給初級(jí)開發(fā)者們?cè)斐闪撕芏嗵魬?zhàn)叮雳。他們所面對(duì)的其中一個(gè)關(guān)鍵挑戰(zhàn)是想暗,怎樣確保單例類的行為是單例?也就是說帘不,無論任何原因说莫,如何防止單例類有多個(gè)實(shí)例。在整個(gè)應(yīng)用生命周期中寞焙,要保證只有一個(gè)單例類的實(shí)例被創(chuàng)建储狭,雙重檢查鎖(Double checked locking of Singleton)是一種實(shí)現(xiàn)方法。顧名思義捣郊,在雙重檢查鎖中辽狈,代碼會(huì)檢查兩次單例類是否有已存在的實(shí)例,一次加鎖一次不加鎖模她,一次確保不會(huì)有多個(gè)實(shí)例被創(chuàng)建稻艰。
順便提一下,在JDK1.5中侈净,Java修復(fù)了其內(nèi)存模型的問題尊勿。在JDK1.5之前僧凤,這種方法會(huì)有問題。本文中元扔,我們將會(huì)看到怎樣用Java實(shí)現(xiàn)雙重檢查鎖的單例類躯保,為什么Java 5之前的版本雙重檢查鎖會(huì)有問題,以及怎么解決這個(gè)問題澎语。順便說一下途事,這也是重要的面試要點(diǎn),我曾經(jīng)在金融業(yè)和服務(wù)業(yè)的公司面試被要求手寫雙重檢查鎖實(shí)現(xiàn)單例模式擅羞、相信我尸变,這很棘手,除非你清楚理解了你在做什么减俏。你也可以閱讀我的完整列表“單例模式設(shè)計(jì)問題”來更好的準(zhǔn)備面試召烂。
為什么你需要雙重檢查鎖來實(shí)現(xiàn)單例類?
一個(gè)常見情景娃承,單例類在多線程環(huán)境中違反契約奏夫。如果你要一個(gè)新手寫出單例模式,可能會(huì)得到下面的代碼:
private static Singleton _instance;
public static Singleton getInstance() {
if (_instance == null) {
_instance = new Singleton();
}
return _instance;
}
然后历筝,當(dāng)你指出這段代碼在超過一個(gè)線程并行被調(diào)用的時(shí)候會(huì)創(chuàng)建多個(gè)實(shí)例的問題時(shí)酗昼,他很可能會(huì)把整個(gè)getInstance()方法設(shè)為同步(synchronized),就像我們展示的第二段示例代碼getInstanceTS()方法一樣梳猪。盡管這樣做到了線程安全麻削,并且解決了多實(shí)例問題,但并不高效春弥。在任何調(diào)用這個(gè)方法的時(shí)候碟婆,你都需要承受同步帶來的性能開銷,然而同步只在第一次調(diào)用的時(shí)候才被需要惕稻,也就是單例類實(shí)例創(chuàng)建的時(shí)候。這將促使我們使用雙重檢查鎖模式(double checked locking pattern)蝙叛,一種只在臨界區(qū)代碼加鎖的方法俺祠。程序員稱其為雙重檢查鎖,因?yàn)闀?huì)有兩次檢查 _instance == null借帘,一次不加鎖蜘渣,另一次在同步塊上加鎖。這就是使用Java雙重檢查鎖的示例:
public static Singleton getInstanceDC() {
if (_instance == null) {? ? ? ? ? ? ? ? // Single Checked
synchronized (Singleton.class) {
if (_instance == null) {? ? ? ? // Double checked
_instance = new Singleton();
}
}
}
return _instance;
}
這個(gè)方法表面上看起來很完美肺然,你只需要付出一次同步塊的開銷蔫缸,但它依然有問題。除非你聲明_instance變量時(shí)使用了volatile關(guān)鍵字际起。沒有volatile修飾符拾碌,可能出現(xiàn)Java中的另一個(gè)線程看到個(gè)初始化了一半的_instance的情況吐葱,但使用了volatile變量后,就能保證先行發(fā)生關(guān)系(happens-before relationship)校翔。對(duì)于volatile變量_instance弟跑,所有的寫(write)都將先行發(fā)生于讀(read),在Java 5之前不是這樣防症,所以在這之前使用雙重檢查鎖有問題∶霞現(xiàn)在,有了先行發(fā)生的保障(happens-before guarantee)蔫敲,你可以安全地假設(shè)其會(huì)工作良好饲嗽。另外,這不是創(chuàng)建線程安全的單例模式的最好方法奈嘿,你可以使用枚舉實(shí)現(xiàn)單例模式貌虾,這種方法在實(shí)例創(chuàng)建時(shí)提供了內(nèi)置的線程安全。
這是個(gè)用Java創(chuàng)建線程安全單例模式的有爭(zhēng)議的方法指么,使用枚舉實(shí)現(xiàn)單例類更簡(jiǎn)單有效酝惧。我并不建議你像這樣實(shí)現(xiàn)單例模式,因?yàn)橛肑ava有許多更好的方式伯诬。但是晚唇,這個(gè)問題有歷史意義,也教授了并發(fā)是如何引入一些微妙錯(cuò)誤的盗似。正如之前所說哩陕,這是面試中非常重要的一點(diǎn)。在去參加任何Java面試之前赫舒,要練習(xí)手寫雙重檢查鎖實(shí)現(xiàn)單例類悍及。這將增強(qiáng)你發(fā)現(xiàn)Java程序員們所犯編碼錯(cuò)誤的洞察力。另外接癌,在現(xiàn)在的測(cè)試驅(qū)動(dòng)開發(fā)中心赶,單例模式由于難以被模擬其行為而被視為反模式(anti pattern),所以如果你是測(cè)試驅(qū)動(dòng)開發(fā)的開發(fā)者缺猛,最好避免使用單例模式缨叫。