面試官所認為的單例模式

咖啡拿鐵發表於2018-07-19

單例模式是23種GOF模式中最簡單,也是最經常出現的一種設計模式,也是面試官最常愛考的一種模式,為什麼呢? 因為單例模式足夠簡單,編寫一個單例模式程式碼幾分鐘就能搞定,所以設計模式中面試官通常會選取單例模式作為出題。 下面把單例模式分幾個點,分別說說哪些地方面試官能考你?

單例模式的意義

通常面試官會很籠統的問你,什麼是單例模式?單例模式用來解決了什麼痛點?沒有單例模式我們會怎麼辦?單例模式他有什麼缺點嗎?

單例模式是最簡單的設計模式之一,屬於建立型模式,它提供了一種建立物件的方式,確保只有單個物件被建立。這個設計模式主要目的是想在整個系統中只能出現類的一個例項,即一個類只有一個物件。 單例模式的解決的痛點就是節約資源,節省時間從兩個方面看:

1.由於頻繁使用的物件,可以省略建立物件所花費的時間,這對於那些重量級的物件而言,是很重要的.

2.因為不需要頻繁建立物件,我們的GC壓力也減輕了,而在GC中會有STW(stop the world),從這一方面也節約了GC的時間 單例模式的缺點:簡單的單例模式設計開發都比較簡單,但是複雜的單例模式需要考慮執行緒安全等併發問題,引入了部分複雜度。

擴充套件:從你的回答中能進行哪些擴充套件呢?我們談到了GC,有可能這時候就會問你GC,STW等知識。談缺點的時候談到了複雜的單例模式, 這個時候可能會問你讓你設計一個優秀的單例模式你會怎麼設計,會怎麼實現?

單例模式的設計

通常這裡面試官會問你單例模式怎麼設計,需要看重哪些方面?一般來說單例模式有哪些實現方式?

設計單例模式的時候一般需要考慮幾種因素:

-執行緒安全 -延遲載入 -程式碼安全:如防止序列化攻擊,防止反射攻擊(防止反射進行私有方法呼叫) -效能因素

一般來說,我們去網上百度去搜大概有7,8種實現,,下面列舉一下需要重點知道的 餓漢,懶漢(執行緒安全,執行緒非安全),雙重檢查(DCL)(重點),內部類,以及列舉(重點), 下面比對下各個實現:

 

執行緒安全

併發效能好

可以延遲載入

序列化/反序列化安全

能抵禦反射攻擊

餓漢式

Y

Y

 

 

 

懶漢式

不加鎖

 

Y

Y

 

 

加鎖的

Y

 

Y

 

 

DCL

Y

Y

Y

 

 

靜態內部類

Y

Y

Y

 

 

列舉

Y

Y

 

Y

Y

擴充套件:我們上面說到了各個模式的實現,這個時候很有可能會叫你手寫各個模式的程式碼。當然也有可能會問你執行緒安全,程式碼安全等知識。

餓漢模式

餓漢模式的程式碼如下:

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return instance;
    }
}
複製程式碼

餓漢模式程式碼比較簡單,物件在類中被定義為private static,通過getInstance(),通過java的classLoader機制保證了單例物件唯一。 擴充套件:

有可能會問instance什麼時候被初始化?

Singleton類被載入的時候就會被初始化,java虛擬機器規範雖然沒有強制性約束在什麼時候開始類載入過程,但是對於類的初始化,虛擬機器規範則嚴格規定了有且只有四種情況必須立即對類進行初始化,遇到new、getStatic、putStatic或invokeStatic這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。 生成這4條指令最常見的java程式碼場景是:1)使用new關鍵字例項化物件2)讀取一個類的靜態欄位(被final修飾、已在編譯期把結果放在常量池的靜態欄位除外)3)設定一個類的靜態欄位(被final修飾、已在編譯期把結果放在常量池的靜態欄位除外)4)呼叫一個類的靜態方法

class的生命週期?

class的生命週期一般來說會經歷載入、連線、初始化、使用、和解除安裝五個階段

class的載入機制

這裡可以聊下classloader的雙親委派模型。

雙重檢查DCL

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}  
複製程式碼

synchronized同步塊裡面能夠保證只建立一個物件。但是通過在synchronized的外面增加一層判斷,就可以在物件一經建立以後,不再進入synchronized同步塊。這種方案不僅減小了鎖的粒度,保證了執行緒安全,效能方面也得到了大幅提升。

同時這裡要注意一定要說volatile,這個很關鍵,volatile一般用於多執行緒的可見性,但是這裡是用來防止指令重排序的。

擴充套件:

為什麼需要volatile?volatile有什麼用?

  • 首先要回答可見性,這個是毋庸質疑的,然後可能又會考到java記憶體模型。
  • 防止指令重排序: 防止new Singleton時指令重排序導致其他執行緒獲取到未初始化完的物件。instance = new Singleton()這句,這並非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。1.給 instance 分配記憶體2.呼叫 Singleton 的建構函式來初始化成員變數3.將instance物件指向分配的記憶體空間(執行完這步 instance 就為非 null 了) 但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,被執行緒二搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以執行緒二會直接返回 instance,然後使用,然後報錯。
  • 順便也可以說下volatie原理用記憶體屏障

講講synchronized和volatile的區別

這裡可以從synchroized能保證原子性,volatile不能保證說起,以及講下synchroized是重量級鎖,甚至可以所以下他和Lock的區別等等。

執行緒安全一般怎麼實現的?

  • 互斥同步。如lock,synchroized
  • 非阻塞同步。如cas。
  • 不同步。如threadLocal,區域性變數。

列舉類

public enum Singleton{
    INSTANCE;
}
複製程式碼

預設列舉例項的建立是執行緒安全的,所以不需要擔心執行緒安全的問題。同時他也是《Effective Java》中推薦的模式。最後通過列舉類,他能自動避免序列化/反序列化攻擊,以及反射攻擊(列舉類不能通過反射生成)。

總結

單例模式雖然看起來簡單,但是設計的Java基礎知識非常多,如static修飾符、synchronized修飾符、volatile修飾符、enum等。這裡的每一個知識點都可以變成面試官下手的考點,而單例只是作為一個引子,考到最後看你到底掌握了多少。看你的廣度和深度到底是怎麼樣的。

最後這篇文章被我收錄於JGrowing,一個全面,優秀,由社群一起共建的Java學習路線,如果您想參與開源專案的維護,可以一起共建,github地址為:github.com/javagrowing… 麻煩給個小星星喲。

如果大家覺得這篇文章對你有幫助,或者想提前獲取後續章節文章,或者你有什麼疑問想提供1v1免費vip服務,都可以關注我的公眾號你的關注和轉發是對我最大的支援,O(∩_∩)O:

面試官所認為的單例模式

相關文章