Java面試官:寫個單例模式,面對這個問題,到底是想考察什麼?

面試官很喜歡請候選人寫一個單例模式,貌似波瀾不驚的問題能考察出很多 Java 基礎問題。

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面試官:寫個單例模式,面對這個問題,到底是想考察什麼?

 

問題點:

上述設計貌似巧妙,實際上卻是有問題的:如下簡單的賦值語句,在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 ;

但是,這種雙重檢查的代碼還是令人不爽,有沒有更優雅的實現形式呢?

Java面試官:寫個單例模式,面對這個問題,到底是想考察什麼?

 

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 虛擬機會保證對象構造完成優先與線程訪問,防止多線程衝突問題。

Java面試官:寫個單例模式,面對這個問題,到底是想考察什麼?

 

總結

面試官考察單例模式,着眼點並不在於考察設計模式本身,面試官預留的“坑”在多線程訪問方面:

  • 初級候選人應當正確寫出模式一,或者模式二,具備設計模式和多線程訪問的基本知識。
  • 中高級候選人應當正確理解 volatile synchronized final 等基本語義,具備JAVA 內存模型的基本知識,瞭解指令重排,變量可見性等概念,設計線程安全的類。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章