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

設計模式-單例模式

《巫師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多線程吧。

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

重點回顧

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

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



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章