Sunny 公司開發(fā)人員使用單例模式實(shí)現(xiàn)了負(fù)載均衡器的設(shè)計(jì),但是在實(shí)際使用中出現(xiàn)了一個(gè)非常嚴(yán)重的問題,當(dāng)負(fù)載均衡器在啟動(dòng)過程中用戶再次啟動(dòng)該負(fù)載均衡器時(shí),系統(tǒng)無任何異常,但當(dāng)客戶端提交請求時(shí)出現(xiàn)請求分發(fā)失敗,通過仔細(xì)分析發(fā)現(xiàn)原來系統(tǒng)中還是存在多個(gè)負(fù)載均衡器對象,導(dǎo)致分發(fā)時(shí)目標(biāo)服務(wù)器不一致,從而產(chǎn)生沖突。為什么會這樣呢?Sunny 公司開發(fā)人員百思不得其解。
現(xiàn)在我們對負(fù)載均衡器的實(shí)現(xiàn)代碼進(jìn)行再次分析,當(dāng)?shù)谝淮握{(diào)用 getLoadBalancer() 方法創(chuàng)建并啟動(dòng)負(fù)載均衡器時(shí),instance 對象為 null 值,因此系統(tǒng)將執(zhí)行代碼 instance= new LoadBalancer(),在此過程中,由于要對 LoadBalancer 進(jìn)行大量初始化工作,需要一段時(shí)間來創(chuàng)建 LoadBalancer 對象。而在此時(shí),如果再一次調(diào)用 getLoadBalancer() 方法(通常發(fā)生在多線程環(huán)境中),由于instance尚未創(chuàng)建成功,仍為 null 值,判斷條件(instance== null)為真值,因此代碼 instance= new LoadBalancer() 將再次執(zhí)行,導(dǎo)致最終創(chuàng)建了多個(gè) instance 對象,這違背了單例模式的初衷,也導(dǎo)致系統(tǒng)運(yùn)行發(fā)生錯(cuò)誤。
如何解決該問題?我們至少有兩種解決方案,在正式介紹這兩種解決方案之前,先介紹一下單例類的兩種不同實(shí)現(xiàn)方式,餓漢式單例類和懶漢式單例類。
餓漢式單例類是實(shí)現(xiàn)起來最簡單的單例類,餓漢式單例類結(jié)構(gòu)圖如圖所示:
http://wiki.jikexueyuan.com/project/design-pattern-creation/images/1333305889_1823.gif" alt="" />
從圖中可以看出,由于在定義靜態(tài)變量的時(shí)候?qū)嵗瘑卫?,因此在類加載的時(shí)候就已經(jīng)創(chuàng)建了單例對象,代碼如下所示:
class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() { }
public static EagerSingleton getInstance() {
return instance;
}
}
當(dāng)類被加載時(shí),靜態(tài)變量 instance 會被初始化,此時(shí)類的私有構(gòu)造函數(shù)會被調(diào)用,單例類的唯一實(shí)例將被創(chuàng)建。如果使用餓漢式單例來實(shí)現(xiàn)負(fù)載均衡器 LoadBalancer 類的設(shè)計(jì),則不會出現(xiàn)創(chuàng)建多個(gè)單例對象的情況,可確保單例對象的唯一性。
除了餓漢式單例,還有一種經(jīng)典的懶漢式單例,也就是前面的負(fù)載均衡器 LoadBalancer 類的實(shí)現(xiàn)方式。懶漢式單例類結(jié)構(gòu)圖如圖所示:
http://wiki.jikexueyuan.com/project/design-pattern-creation/images/1333305983_8045.gif" alt="" />
從圖中可以看出,懶漢式單例在第一次調(diào)用 getInstance() 方法時(shí)實(shí)例化,在類加載時(shí)并不自行實(shí)例化,這種技術(shù)又稱為延遲加載(Lazy Load)技術(shù),即需要的時(shí)候再加載實(shí)例,為了避免多個(gè)線程同時(shí)調(diào)用 getInstance() 方法,我們可以使用關(guān)鍵字 synchronized,代碼如下所示:
class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() { }
synchronized public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
該懶漢式單例類在 getInstance() 方法前面增加了關(guān)鍵字 synchronized 進(jìn)行線程鎖,以處理多個(gè)線程同時(shí)訪問的問題。但是,上述代碼雖然解決了線程安全問題,但是每次調(diào)用 getInstance() 時(shí)都需要進(jìn)行線程鎖定判斷,在多線程高并發(fā)訪問環(huán)境中,將會導(dǎo)致系統(tǒng)性能大大降低。如何既解決線程安全問題又不影響系統(tǒng)性能呢?我們繼續(xù)對懶漢式單例進(jìn)行改進(jìn)。事實(shí)上,我們無須對整個(gè) getInstance() 方法進(jìn)行鎖定,只需對其中的代碼“instance = new LazySingleton();”進(jìn)行鎖定即可。因此 getInstance() 方法可以進(jìn)行如下改進(jìn):
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
instance = new LazySingleton();
}
}
return instance;
}
問題貌似得以解決,事實(shí)并非如此。如果使用以上代碼來實(shí)現(xiàn)單例,還是會存在單例對象不唯一。原因如下:
假如在某一瞬間線程A和線程B都在調(diào)用 getInstance() 方法,此時(shí) instance 對象為 null 值,均能通過 instance == null 的判斷。由于實(shí)現(xiàn)了 synchronized 加鎖機(jī)制,線程 A 進(jìn)入 synchronized 鎖定的代碼中執(zhí)行實(shí)例創(chuàng)建代碼,線程 B 處于排隊(duì)等待狀態(tài),必須等待線程 A 執(zhí)行完畢后才可以進(jìn)入 synchronized 鎖定代碼。但當(dāng) A 執(zhí)行完畢時(shí),線程B并不知道實(shí)例已經(jīng)創(chuàng)建,將繼續(xù)創(chuàng)建新的實(shí)例,導(dǎo)致產(chǎn)生多個(gè)單例對象,違背單例模式的設(shè)計(jì)思想,因此需要進(jìn)行進(jìn)一步改進(jìn),在 synchronized 中再進(jìn)行一次(instance == null)判斷,這種方式稱為雙重檢查鎖定(Double-Check Locking)。使用雙重檢查鎖定實(shí)現(xiàn)的懶漢式單例類完整代碼如下所示:
class LazySingleton {
private volatile static LazySingleton instance = null;
private LazySingleton() { }
public static LazySingleton getInstance() {
//第一重判斷
if (instance == null) {
//鎖定代碼塊
synchronized (LazySingleton.class) {
//第二重判斷
if (instance == null) {
instance = new LazySingleton(); //創(chuàng)建單例實(shí)例
}
}
}
return instance;
}
}
需要注意的是,如果使用雙重檢查鎖定來實(shí)現(xiàn)懶漢式單例類,需要在靜態(tài)成員變量 instance 之前增加修飾符 volatile,被 volatile 修飾的成員變量可以確保多個(gè)線程都能夠正確處理,且該代碼只能在 JDK 1.5 及以上版本中才能正確執(zhí)行。由于 volatile 關(guān)鍵字會屏蔽 Java 虛擬機(jī)所做的一些代碼優(yōu)化,可能會導(dǎo)致系統(tǒng)運(yùn)行效率降低,因此即使使用雙重檢查鎖定來實(shí)現(xiàn)單例模式也不是一種完美的實(shí)現(xiàn)方式。
IBM 公司高級軟件工程師 Peter Haggar 2004 年在 IBM developerWorks 上發(fā)表了一篇名為《雙重檢查鎖定及單例模式——全面理解這一失效的編程習(xí)語》的文章,對JDK 1.5 之前的雙重檢查鎖定及單例模式進(jìn)行了全面分析和闡述。
餓漢式單例類在類被加載時(shí)就將自己實(shí)例化,它的優(yōu)點(diǎn)在于無須考慮多線程訪問問題,可以確保實(shí)例的唯一性;從調(diào)用速度和反應(yīng)時(shí)間角度來講,由于單例對象一開始就得以創(chuàng)建,因此要優(yōu)于懶漢式單例。但是無論系統(tǒng)在運(yùn)行時(shí)是否需要使用該單例對象,由于在類加載時(shí)該對象就需要?jiǎng)?chuàng)建,因此從資源利用效率角度來講,餓漢式單例不及懶漢式單例,而且在系統(tǒng)加載時(shí)由于需要?jiǎng)?chuàng)建餓漢式單例對象,加載時(shí)間可能會比較長。
懶漢式單例類在第一次使用時(shí)創(chuàng)建,無須一直占用系統(tǒng)資源,實(shí)現(xiàn)了延遲加載,但是必須處理好多個(gè)線程同時(shí)訪問的問題,特別是當(dāng)單例類作為資源控制器,在實(shí)例化時(shí)必然涉及資源初始化,而資源初始化很有可能耗費(fèi)大量時(shí)間,這意味著出現(xiàn)多線程同時(shí)首次引用此類的機(jī)率變得較大,需要通過雙重檢查鎖定等機(jī)制進(jìn)行控制,這將導(dǎo)致系統(tǒng)性能受到一定影響。