面試:用 Java 實現一個 Singleton 模式
面試系列更新後,終於迎來了我們的第一期,我們也將貼近《劍指 Offer》的題目給大家帶來 Java 的講解,個人還是非常推薦《劍指 Offer》作為面試必刷的書籍的,這不,再一次把這本書分享給大家,PDF 版本在公眾號後臺回覆「劍指Offer」即可獲取。
我們在面試中總會遇到不少設計模式的問題,而設計模式中的 Singleton 模式又是我們最容易出現的考題,大多數人可能在此前已經有充分的瞭解,但不少人僅僅是停留在比較淺顯的層次,今天我們就結合《劍指 Offer》給大家帶來更加深入的講解。
題目:請用 Java 手寫一個單例模式程式碼,希望儘可能考慮地全面。
不論是 Java 還是 Android 中單例模式肯定是我們經常用到的,所以這道題可能大多數人會第一時間想到餓漢式程式碼。
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return INSTANCE;
}
}
上面是典型的餓漢式寫法,因為單例的例項被宣告成 static 和 final 變量了,所以在第一次載入類到記憶體中時就會初始化,所以也不會存在多執行緒問題,但它的缺點非常顯而易見,也經常為人詬病。這明顯不是一種懶載入模式(lazy initialization),就因為它是 static 和 final 的,所以類會在載入後就被初始化,導致我們程式碼的健壯性很差,假如後面更改需求,希望在 getInstance()
之前呼叫某個方法給它設定引數,這個就明顯不符合使用場景了,面試官極有可能在看到這個程式碼後覺得你就是一個只知道完成功能沒有大局觀的人。
當然還會有不少人直接採用我們的懶漢式程式碼,這樣就解決了延展性和懶載入了。
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
上述程式碼可能是大多數面試者的解法,包括教科書上也是這麼教我們的,但這段程式碼卻存在了一個致命的問題,那就是當多個執行緒並行呼叫 getInstance()
的時候,就會建立多個例項,這顯然違背了面試官的意思。正好面試官加了一句希望儘可能考慮地全面,所以這樣的程式碼肯定不能虜獲面試官的芳心。
既然要執行緒安全,那我直接加鎖唄。於是並有了下面的程式碼。他們也是懶漢式的,只不過執行緒安全了。
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
這樣的解法實現了執行緒安全,但它並不是那麼高效,因為在任何時候只能有一個執行緒去呼叫 getInstance()
方法,但實際上加鎖操作也是耗時的,我們應該儘量地避免使用它。所以自然就引出了雙重檢驗鎖。
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
這段程式碼看起來很完美,很可惜,它是有問題。主要在於instance = new Singleton()這句,這並非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。
給 instance 分配記憶體
呼叫 Singleton 的建構函式來初始化成員變數
將 instance 物件指向分配的記憶體空間(執行完這步 instance 就為非 null 了)
但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,被執行緒二搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以執行緒二會直接返回 instance,然後使用,然後順理成章地報錯。
我們只需要將 instance 變數宣告成 volatile 就可以了。
public class Singleton {
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
有些人認為使用 volatile
的原因是可見性,也就是可以保證執行緒在本地不會存有 instance 的副本,每次都是去主記憶體中讀取。但其實是不對的。使用 volatile
的主要原因是其另一個特性:禁止指令重排序優化。也就是說,在 volatile
變數的賦值操作後面會有一個記憶體屏障(生成的彙編程式碼上),讀操作不會被重排序到記憶體屏障之前。比如上面的例子,取操作必須在執行完 1-2-3 之後或者 1-3-2 之後,不存在執行到 1-3 然後取到值的情況。從「先行發生原則」的角度理解的話,就是對於一個 volatile 變數的寫操作都先行發生於後面對這個變數的讀操作(這裡的“後面”是時間上的先後順序)。
但是特別注意在 Java 5 以前的版本使用了 volatile
的雙檢鎖還是有問題的。其原因是 Java 5 以前的 JMM (Java 記憶體模型)是存在缺陷的,即時將變數宣告成 volatile 也不能完全避免重排序,主要是 volatile 變數前後的程式碼仍然存在重排序問題。這個 volatile
遮蔽重排序的問題在 Java 5 中才得以修復,所以在這之後才可以放心使用 volatile
。
那麼,有沒有一種既有懶載入,又保證了執行緒安全,還簡單的方法呢?
當然有,靜態內部類,就是一種我們想要的方法。我們完全可以把 Singleton 例項放在一個靜態內部類中,這樣就避免了靜態例項在 Singleton 類載入的時候就建立物件,並且由於靜態內部類只會被載入一次,所以這種寫法也是執行緒安全的。
public class Singleton {
private static class Holder {
private static Singleton INSTANCE = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
這是我比較推薦的解法,這種寫法用 JVM 本身的機制保證了執行緒安全的問題,同時讀取例項的時候也不會進行同步,沒什麼效能缺陷,還不依賴 JDK 版本。
雖說如此,但看《Effective Java》中第三點來說,還是有必要提醒一下:享有特權的客戶端可以藉助 AccessibleObject.setAccessible
方法,通過反射機制來呼叫私有構造器。如果需要抵禦這種攻擊,可以修改構造器,讓它在被要求建立第二個例項的時候丟擲異常。
《Effective Java 中文版》PDF 在公眾號後臺回覆「Effective Java」即可獲取。
我們其實還有更簡單的列舉單例。
用過列舉寫單例的人都說:用列舉寫單例真是太簡單了。下面的這段程式碼就是宣告列舉單例的通常做法。
public enum EasySingleton{
INSTANCE;
}
這是從 Java 1.5 發行版本後就可以實用的單例方法,我們可以通過 EasySingleton.INSTANCE
來訪問例項,這比呼叫 getInstance()
方法簡單多了。建立列舉預設就是執行緒安全的,所以不需要擔心 double checked locking,而且還能防止反序列化導致重新建立新的物件。但是還是很少看到有人這樣寫,可能是因為不太熟悉吧。
總結
一個總結肯定是必不可少的,上面也只是列舉了我們常見的單例實現方式。當然也不完全,比如我們還可以用 static 程式碼塊的方式實現懶漢式程式碼,但這裡就不一一例舉了。
就我個人而言,我還是比較推薦用靜態內部類的方式使用單例模式,如果涉及到反序列化建立物件的話,不妨也試試列舉唄~
文章參考連結:http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/