面試官很喜歡請候選人寫一個單例模式,貌似波瀾不驚的問題能考察出很多 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 內存模型的基本知識,瞭解指令重排,變量可見性等概念,設計線程安全的類。