??我擔(dān)任面試官時其监,很喜歡請候選人寫一個單例模式,貌似波瀾不驚的問題能考察出很多 Java 基礎(chǔ)問題乾忱。
1 基礎(chǔ)單例模式 (正確姿勢)
??首先面試官請候選人寫一個單例模式,于是很多同學(xué)就會寫出如下代碼:
public class SingleInstance {
private static SingleInstance instance = new SingleInstance();
private SingleInstance() {}
public static SingleInstance getInstance() {
return instance;
}
}
??恭喜你历极,這是最基礎(chǔ)的線程安全的單例模式窄瘟,答對了。
要點(diǎn):
- 單例模式需要有一個 private 構(gòu)造函數(shù)趟卸,避免客戶端直接 new 出對象蹄葱;
- 靜態(tài)方法 getInstance() 需要考慮多線程訪問時的競爭問題氏义,但是靜態(tài)成員變量在對象構(gòu)造時生成,優(yōu)先與實(shí)例方法的調(diào)用图云,于是多線程沖突被巧妙的避免了惯悠。
2 延遲構(gòu)造的單例模式(正確姿勢但略有瑕疵)
??方法1中的實(shí)例是在構(gòu)造時創(chuàng)建的,于是竣况,面試官繼續(xù)提問克婶,如果instance需要延遲構(gòu)造,需要怎么修改丹泉?
??于是情萤,LazyInit的單例模式如下,使用時再構(gòu)造對象摹恨。
要點(diǎn):
- getInstance 是一個同步方法(synchronized)筋岛,使用對象鎖,避免多線程導(dǎo)致的問題晒哄。
public class SingleInstance {
private static SingleInstance instance ;
private SingleInstance() {}
public static synchronized SingleInstance getInstance() {
if (instance == null) {
instance = new SingleInstance();
}
return instance;
}
}
2.1 延遲構(gòu)造的單例模式(錯誤姿勢)
??然后睁宰,面試官繼續(xù)提問,這種實(shí)現(xiàn)方式有效率問題寝凌,例如非首次調(diào)用getInstance時柒傻,大量線程只希望獲取一個已經(jīng)構(gòu)造完成的對象,但是也被迫等待硫兰,順序完成诅愚。如何修改能提高效率。
??于是劫映,網(wǎng)上流傳很廣泛违孝,可以說臭名昭著雙重檢查鎖(Double Checked Lock, DCL)的方案很可能會被寫出來:
public class SingleInstance {
private static SingleInstance instance ;
private SingleInstance() {}
public static SingleInstance getInstance() {
if (instance == null) {
synchronized (SingleInstance.class) {
if (instance == null) {
instance = new SingleInstance();
}
}
}
return instance;
}
}
要點(diǎn):
- DCL模式去掉了 getInstance 的 synchronized 修飾符泳赋,這樣instance != null 時雌桑,大量線程不用獲取鎖并等待,提高了效率祖今;
- 如果 instance == null 校坑,獲取class 的類鎖,初始化 instance千诬。
問題點(diǎn):
?? 上述設(shè)計(jì)貌似巧妙,實(shí)際上卻是有問題的:如下簡單的賦值語句徐绑,在JAVA中并不是原子操作邪驮。
instance = new SingleInstance();
?? 該語句可以抽象為如下三個操作,而這三個操作中 2 和 3 可能發(fā)生指令重排:先給 instance 分配一個內(nèi)存傲茄,再對內(nèi)存進(jìn)程初始化毅访。
memory =allocate(); //1:分配對象的內(nèi)存空間
ctorInstance(memory); //2:初始化對象
instance = memory; //3:設(shè)置instance指向剛分配的內(nèi)存地址
?? 于是回過頭來看DCL形式的方案:
- 線程A 在初始化 instance 對象的時沮榜,給instance分配了內(nèi)存,但并未完成初始化喻粹;
- 線程B 判斷 instance 對象不為空蟆融,結(jié)果取走了一個未初始化完成的 instance;類似 C 語言中常見的野指針現(xiàn)象守呜。
2.2 DCL單例模式(正確姿勢)
?? 那么正確的 DCL 應(yīng)該如何修改型酥。在 JAVA 1.5 版本之后,volatile 關(guān)鍵字可以保證字段可見性的同時弛饭,防止編譯器進(jìn)行指令重排冕末。但是volatile并不能保證操作的原子性,所以鎖還是要加的侣颂。上述 DCL 模式修改一行即可:
private static volatile SingleInstance instance ;
?? 但是档桃,這種雙重檢查的代碼還是令人不爽,有沒有更優(yōu)雅的實(shí)現(xiàn)形式呢憔晒?
3 延遲初始化占位類模式 (正確姿勢)
??《Java 并發(fā)編程實(shí)踐》中提供了一種Holder類的的模式藻肄,很好的解決了延遲加載和多線程訪問的問題:
public class SingleInstance {
private static class SingleInstanceHolder {
public static SingleInstance instance = new SingleInstance();
}
private SingleInstance() {};
public static SingleInstance getInstance() {
return SingleInstanceHolder.instance;
}
}
要點(diǎn):
- 提供一個靜態(tài)內(nèi)部類 Holder,getInstance時才會Holder對象才會構(gòu)造拒担;Java 虛擬機(jī)會保證對象構(gòu)造完成優(yōu)先與線程訪問嘹屯,防止多線程沖突問題。
總結(jié)
??面試官考察單例模式从撼,著眼點(diǎn)并不在于考察設(shè)計(jì)模式本身州弟,面試官預(yù)留的“坑”在多線程訪問方面:
- 初級候選人應(yīng)當(dāng)正確寫出模式一,或者模式二低零,具備設(shè)計(jì)模式和多線程訪問的基本知識婆翔。
- 中高級候選人應(yīng)當(dāng)正確理解 volatile synchronized final 等基本語義,具備JAVA 內(nèi)存模型的基本知識掏婶,了解指令重排啃奴,變量可見性等概念,設(shè)計(jì)線程安全的類雄妥。