《我是面試官》設計模式-單例模式

Leo_Lei發表於2021-09-19

設計模式-單例模式

《巫師3》中,陪著主人公南征北戰的坐騎,不管你何時何地召喚它,它永遠只有一個名字——蘿蔔。




大家好,我是左耳朵梵高。文章首發於微信公眾號「左耳朵梵高」,歡迎關注,和我一起持續學習,終身成長。 ---- 生活不只眼前的苟且,還有詩和遠方。

面試開始

HR :來了一個面試Java的,我讓他在小會議室等著了。

面試官 :好的,我就來。

面試官用一次性紙杯倒了杯水,夾著Mac,進了小會議室。看見一個20出頭的精神小夥,帶著黑框眼鏡,髮量誘人,像極了N年前的自己,風華正茂,書生意氣。

面試官 :你好,先喝杯水吧。(不給應聘者倒水的公司都是不靠譜的)我看你簡歷上寫著精通設計模式,要不我們就聊聊設計模式吧。

應聘者 :可以呀。

一句輕描淡寫的“可以呀”,但經驗豐富的面試官還是發現了平靜面容下,應聘者的一絲絲竊喜,好像很胸有成竹的樣子。

面試官 :那就說說,你平時都用了哪些設計模式吧?

應聘者 :(內心狂喜ing)我平時使用最多的設計模式有單例模式。單例模式屬於23種設計模式中的建立型的設計模式。23種設計模式可以分為3種:建立型、結構型和行為型。單例模式確保了一個類只有一個例項。單例模式有5種實現方式:懶漢式、餓漢式、Double-Check方式、靜態內部類方式、列舉方式。

面試官 :嗯,你對單例模式瞭解的不錯嘛。你先說下為什麼要使用單例模式吧。

應聘者 :單例模式其實很簡單,就是一個類只能建立一個例項。在程式中,有一些物件只需要一個,比如說:執行緒池、快取、對話方塊、登錄檔、日誌物件、充當印表機、顯示卡等裝置驅動程式的物件。事實上,這一類物件只能有一個例項,如果製造出多個例項就可能會導致一些問題的產生,比如:程式的行為異常、資源使用過量、或者不一致性的結果。還有些業務上就只會有一個,比如公司主體等。

面試官 :那如何實現一個單例呢?

應聘者 :實現單例有好幾種方式,有餓漢式、懶漢式、靜態內部類,或者使用列舉來實現。使用單例模式,一般把類的建構函式設定為private,避免通過new建立多個示例。我先來說下餓漢方式吧。

應聘者喝了口水,似乎準備開始表演了。

應聘者 :餓漢式實現比較簡單。類有一個靜態的例項,一般取名為instance。在類載入的時候,就會建立並初始化好instance例項。所以,餓漢式是執行緒安全的。

面試官 :你能寫一下具體的實現程式碼嗎?

應聘者很快就在紙上寫出了餓漢式的程式碼實現:

public static Singleton{
    private static final Singleton = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return instance;
    }
}

看的出來,應聘者對餓漢式的程式碼實現很熟悉,編碼風格和命名也很不錯。我在面試的時候,就有幾位應聘者不知道如何給類命名,有的使用Danli,有的使用Single或One。

面試官 :嗯,很不錯。你平時都使用這種方式嗎?

應聘者 :哦,不是的。餓漢式雖然簡單,但是有個問題是,它不支援延遲載入,或者叫按需載入。在系統啟動時,就必須要建立例項。

面試官 :這樣會有什麼問題嗎?

應聘者 :如果例項佔用資源多,比如記憶體佔用高,或者初始化耗時長(比如需要載入各種配置檔案),提前初始化就會造成浪費。應該在用到的時候再去初始化。

面試官 :如果初始化耗時長,等用到的時候再初始化。就可能在使用者請求介面的時候,觸發了這個初始化過程,會導致請求的響應時間很長,甚至超時。對使用者造成影響。所以,究竟是啟動時初始化好,還是延遲初始化好呢?

應聘者 :啊,這個。。。(這個面試官不按套路出牌呀)網上說的都是要延遲載入。

面試官 :還有,如果例項佔用資源多,比如記憶體使用高。如果延遲載入,可能會出現在程式執行一段時間後,因為初始化例項,佔用資源多,出現了OOM,程式崩潰。根據Fast Fail原則,是不是就應該在啟動時初始化例項,如果資源不夠,我們就能快速發現問題,儘快進行修復,而不會讓問題在生產環境中才暴露。

應聘者 :嗯,好像有道理。但我看網上的文章都說這種方式不好。

面試官 :那你覺得哪種方式好呢?

應聘者 :(內心有些搖擺,有些凌亂)

面試官 :那我們再聊聊延遲載入的單例?

應聘者 :嗯嗯,好呀。延遲載入就是在使用的時候才進行初始化,它的程式碼實現是這樣的:

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;
    }
}

面試官 :嗯,不錯嘛。我看getInstance方法中,有多個null判斷,還有個synchronized鎖,能不能解釋一下。

應聘者 :這個叫雙重檢查(Double Check)。加synchronized是為了保證執行緒安全。null判斷是為了提升效能。如果不在前面先判斷instance是否為null,就需要在每次使用時,先獲取鎖,然後釋放鎖,會導致效能瓶頸。

應聘者 :所以使用了雙重檢查,只要instance被建立後,即使再呼叫getInstance,也不會再加鎖了。解決了效能問題。

應聘者 :網上有人說,這種實現方式也有問題。因為指令重排,可能會導致Singleton被new出來後,被賦值給了instance,還沒來得及初始化,就被另一個執行緒使用了,可能會出現NPE錯誤。要解決這個問題,我們需要給instance成員變數新增volatile關鍵字,禁止指令重排。

面試官 :嗯,你對Java指令重排也有了解呀,不錯。關於執行緒安全,我們稍後再仔細聊聊吧。

應聘者 :(不要啊,我就只記住了這一段。待會兒一聊就露餡了啊。。。)

面試官:你知道還有其它實現單例的方式嗎?

應聘者 :還有個靜態內部類方式。它比雙重檢查更加簡單。就是利用Java的靜態內部類。程式碼實現是這樣的:

public class Singleton{
    private Singleton(){}
    private static class SingletonHolder{
        private static final Singleton instance = new Singleton();
    }
    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}

應聘者 :SingletonHolder是一個內部靜態類,當外部Singleton被載入時,並不會建立SingletonHolder例項物件。只有當呼叫getInstance方法時,SingletonHolder才被載入,這個時候才會建立instance。instance的唯一性、建立過程的執行緒安全有JVM虛擬機器來保證。所以,這種實現方法既保證了執行緒安全,又能做到延遲載入。

應聘者 :還有一種使用列舉建立單例的方式。

面試官 :哇,還有嘛。那你再說說吧。

應聘者 :使用列舉應該是最簡單的。它利用了Java列舉型別本身的特點,保證了例項建立的執行緒安全和例項唯一性。程式碼如下:

public enum Singleton{
    INSTANCE;
}

面試官 :你平時都是使用這個方式嗎?

應聘者 :沒有呢。這種方式的確簡單,而且也是《Effective Java》作者推薦的。但是我覺得用列舉來表達一個單例,這種方式比較奇怪。總覺得是一樣投機取巧的方式。

面試官 :哈哈哈。。的確是這樣,開源專案中也很少會使用這種方式,是比較怪。你對單例模式的理解很深入呀,說出了這麼多種實現,不錯不錯。剛才看你對執行緒安全也挺了解的,那我們接下來再聊聊Java多執行緒吧。

應聘者 :(狠狠抽了幾下耳巴子。。。叫你多嘴。。。)

重點回顧

單例模式是面試中經常出現的話題。單例模式本身比較簡單,就是一個類只有一個例項。大部分面試者在面試準備時,都會閱讀單例的相關知識點,比如單例模式的多種實現。

但是,希望大家不要僅僅是背誦,還應該多去理解。本文的面試中,面試官問了一個問題,到底是啟動時初始化好,還是延遲載入好呢?這個問題,大家可以自己思考一下。



相關文章