單例類在Java開發(fā)者中非常常用,但是它給初級開發(fā)者們造成了很多挑戰(zhàn)。他們所面對的其中一個關(guān)鍵挑戰(zhàn)是,怎樣確保單例類的行為是單例?也就是說,無論任何原因,如何防止單例類有多個實例。在整個應(yīng)用生命周期中,要保證只有一個單例類的實例被創(chuàng)建,雙重檢查鎖(Double checked locking of Singleton)是一種實現(xiàn)方法。顧名思義,在雙重檢查鎖中,代碼會檢查兩次單例類是否有已存在的實例,一次加鎖一次不加鎖,一次確保不會有多個實例被創(chuàng)建。
順便提一下,在JDK1.5中,Java修復(fù)了其內(nèi)存模型的問題。在JDK1.5之前,這種方法會有問題。本文中,我們將會看到怎樣用Java實現(xiàn)雙重檢查鎖的單例類,為什么Java 5之前的版本雙重檢查鎖會有問題,以及怎么解決這個問題。順便說一下,這也是重要的面試要點,我曾經(jīng)在金融業(yè)和服務(wù)業(yè)的公司面試被要求手寫雙重檢查鎖實現(xiàn)單例模式、相信我,這很棘手,除非你清楚理解了你在做什么。你也可以閱讀我的完整列表“單例模式設(shè)計問題”來更好的準備面試。
為什么你需要雙重檢查鎖來實現(xiàn)單例類?
一個常見情景,單例類在多線程環(huán)境中違反契約。如果你要一個新手寫出單例模式,可能會得到下面的代碼:
private static Singleton _instance;
public static Singleton getInstance() {
if (_instance == null) {
_instance = new Singleton();
}
return _instance;
}
然后,當你指出這段代碼在超過一個線程并行被調(diào)用的時候會創(chuàng)建多個實例的問題時,他很可能會把整個getInstance()方法設(shè)為同步(synchronized),就像我們展示的第二段示例代碼getInstanceTS()方法一樣。盡管這樣做到了線程安全,并且解決了多實例問題,但并不高效。在任何調(diào)用這個方法的時候,你都需要承受同步帶來的性能開銷,然而同步只在第一次調(diào)用的時候才被需要,也就是單例類實例創(chuàng)建的時候。這將促使我們使用雙重檢查鎖模式(double checked locking pattern),一種只在臨界區(qū)代碼加鎖的方法。程序員稱其為雙重檢查鎖,因為會有兩次檢查 _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;
}
這個方法表面上看起來很完美,你只需要付出一次同步塊的開銷,但它依然有問題。除非你聲明_instance變量時使用了volatile關(guān)鍵字。沒有volatile修飾符,可能出現(xiàn)Java中的另一個線程看到個初始化了一半的_instance的情況,但使用了volatile變量后,就能保證先行發(fā)生關(guān)系(happens-before relationship)。對于volatile變量_instance,所有的寫(write)都將先行發(fā)生于讀(read),在Java 5之前不是這樣,所以在這之前使用雙重檢查鎖有問題。現(xiàn)在,有了先行發(fā)生的保障(happens-before guarantee),你可以安全地假設(shè)其會工作良好。另外,這不是創(chuàng)建線程安全的單例模式的最好方法,你可以使用枚舉實現(xiàn)單例模式,這種方法在實例創(chuàng)建時提供了內(nèi)置的線程安全。
這是個用Java創(chuàng)建線程安全單例模式的有爭議的方法,使用枚舉實現(xiàn)單例類更簡單有效。我并不建議你像這樣實現(xiàn)單例模式,因為用Java有許多更好的方式。但是,這個問題有歷史意義,也教授了并發(fā)是如何引入一些微妙錯誤的。正如之前所說,這是面試中非常重要的一點。在去參加任何Java面試之前,要練習(xí)手寫雙重檢查鎖實現(xiàn)單例類。這將增強你發(fā)現(xiàn)Java程序員們所犯編碼錯誤的洞察力。另外,在現(xiàn)在的測試驅(qū)動開發(fā)中,單例模式由于難以被模擬其行為而被視為反模式(anti pattern),所以如果你是測試驅(qū)動開發(fā)的開發(fā)者,最好避免使用單例模式。