??我擔任面試官時,很喜歡請候選人寫一個單例模式,貌似波瀾不驚的問題能考察出很多 Java 基礎問題。
1 基礎單例模式 (正確姿勢)
??首先面試官請候選人寫一個單例模式,于是很多同學就會寫出如下代碼:
public class SingleInstance {
private static SingleInstance instance = new SingleInstance();
private SingleInstance() {}
public static SingleInstance getInstance() {
return instance;
}
}
??恭喜你,這是最基礎的線程安全的單例模式,答對了。
要點:
- 單例模式需要有一個 private 構造函數,避免客戶端直接 new 出對象;
- 靜態方法 getInstance() 需要考慮多線程訪問時的競爭問題,但是靜態成員變量在對象構造時生成,優先與實例方法的調用,于是多線程沖突被巧妙的避免了。
2 延遲構造的單例模式(正確姿勢但略有瑕疵)
??方法1中的實例是在構造時創建的,于是,面試官繼續提問,如果instance需要延遲構造,需要怎么修改?
??于是,LazyInit的單例模式如下,使用時再構造對象。
要點:
- getInstance 是一個同步方法(synchronized),使用對象鎖,避免多線程導致的問題。
public class SingleInstance {
private static SingleInstance instance ;
private SingleInstance() {}
public static synchronized SingleInstance getInstance() {
if (instance == null) {
instance = new SingleInstance();
}
return instance;
}
}
2.1 延遲構造的單例模式(錯誤姿勢)
??然后,面試官繼續提問,這種實現方式有效率問題,例如非首次調用getInstance時,大量線程只希望獲取一個已經構造完成的對象,但是也被迫等待,順序完成。如何修改能提高效率。
??于是,網上流傳很廣泛,可以說臭名昭著雙重檢查鎖(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;
}
}
要點:
- DCL模式去掉了 getInstance 的 synchronized 修飾符,這樣instance != null 時,大量線程不用獲取鎖并等待,提高了效率;
- 如果 instance == null ,獲取class 的類鎖,初始化 instance。
問題點:
?? 上述設計貌似巧妙,實際上卻是有問題的:如下簡單的賦值語句,在JAVA中并不是原子操作。
instance = new SingleInstance();
?? 該語句可以抽象為如下三個操作,而這三個操作中 2 和 3 可能發生指令重排:先給 instance 分配一個內存,再對內存進程初始化。
memory =allocate(); //1:分配對象的內存空間
ctorInstance(memory); //2:初始化對象
instance = memory; //3:設置instance指向剛分配的內存地址
?? 于是回過頭來看DCL形式的方案:
- 線程A 在初始化 instance 對象的時,給instance分配了內存,但并未完成初始化;
- 線程B 判斷 instance 對象不為空,結果取走了一個未初始化完成的 instance;類似 C 語言中常見的野指針現象。
2.2 DCL單例模式(正確姿勢)
?? 那么正確的 DCL 應該如何修改。在 JAVA 1.5 版本之后,volatile 關鍵字可以保證字段可見性的同時,防止編譯器進行指令重排。但是volatile并不能保證操作的原子性,所以鎖還是要加的。上述 DCL 模式修改一行即可:
private static volatile SingleInstance instance ;
?? 但是,這種雙重檢查的代碼還是令人不爽,有沒有更優雅的實現形式呢?
3 延遲初始化占位類模式 (正確姿勢)
??《Java 并發編程實踐》中提供了一種Holder類的的模式,很好的解決了延遲加載和多線程訪問的問題:
public class SingleInstance {
private static class SingleInstanceHolder {
public static SingleInstance instance = new SingleInstance();
}
private SingleInstance() {};
public static SingleInstance getInstance() {
return SingleInstanceHolder.instance;
}
}
要點:
- 提供一個靜態內部類 Holder,getInstance時才會Holder對象才會構造;Java 虛擬機會保證對象構造完成優先與線程訪問,防止多線程沖突問題。
總結
??面試官考察單例模式,著眼點并不在于考察設計模式本身,面試官預留的“坑”在多線程訪問方面:
- 初級候選人應當正確寫出模式一,或者模式二,具備設計模式和多線程訪問的基本知識。
- 中高級候選人應當正確理解 volatile synchronized final 等基本語義,具備JAVA 內存模型的基本知識,了解指令重排,變量可見性等概念,設計線程安全的類。