面試:一個單例模式,足以把你秒成渣


去面試(對,又去面試)

問:單例模式了解吧,來,拿紙和筆寫一下單例模式。

我心想,這TM不是瞧不起人嗎?我編程十年,能不知道單例模式。

答:(.net 平台下)單例模式有兩種寫法:

第一種:飢餓模式,關鍵點,static readonly

public static readonly SingletonSimple Instance = new SingletonSimple();

第二種:懶加載模式,關鍵點,lock + 兩次判斷

        static readonly object locker = new object();
        static SingletonLazy singleton = null;
        public static SingletonLazy Instance
        {
            get
            {
                if (singleton == null)
                {
                    lock (locker)
                    {
                        if (singleton == null)
                        {
                            singleton = new SingletonLazy();
                        }
                    }
                }

                return singleton;
            }
        }

 我再贈送你一種,第三種:通過IOC容器,注入單例。

 

問:這兩種方式(第一種和第二種)有什么不同嗎?(好戲開始)

答: 懶加載模式的單例是在Instance調用時進行創建。飢餓模式下的單例在程序啟動時創建(這里錯了),浪費資源。

似乎答案就是這樣,好些網文,博主也都是這么寫的,但大家都錯了。(輕信他人,不自己思考,這么基礎的東西居然沒搞明白)

反饋:兩種方式並沒有本質的區別,都是在類調用的時候創建

還沒有完,虐狗模式才剛剛開始。

問:說一下lock的原理

答:對代碼塊加鎖,加鎖的代碼只允許串行執行,防止並發沖突。lock本質上是通過 System.Threading.Monitor實現的,但lock使用比Monitor更簡單,可以自動釋放。

問:那Monitor是如何實現多個線程的阻塞調用的?一個線程執行完,是如何通知下一個線程執行的?有沒有自己實現過一個lock(不使用.net自帶的lock)?

答:......(完全一臉懵逼,根本不知道怎么回答)

問:IOC使用了什么設計模式,IOC是如何控制對象生命周期的?

答:......(還沒從剛才的窘迫中反應過來,更是不知道該說什么)

 

總結:

結合大家的評論和指正,我做一下總結,以及新的認識。

這里只是作為一個自己的面試記錄,以及思考過程,主要是意識到了自己之前在對待靜態變量上的一個錯誤,建議面試時,還是要去背標准答案,成功率更高。

1.直接調用單例類.Instance,使用單例,這兩種方式的單例對象的創建和執行是一樣的。

2.評論中有這么個觀點“飢餓模式是在類加載時創建實例,而懶加載模式是在Instance被調用時創建實例。”

單純從概念上講,這樣說是對的,面試回答問題,這也是一個標准答案。但具體到示例代碼來看,除了調用Instance,沒有其他辦法創建實例。糾結於這兩個概念會陷入“是腳先進的門,還是人先進的門的詭異邏輯。

再有“類加載”是什么概念呢,是像下面這樣,聲明一個變量算類加載嗎?或者調用 typeof(SingletonSimple) 算類加載嗎?我們可以測試一下,這樣並不會觸發對象創建,盡管我們的Instance聲明是靜態的。

SingletonSimple singleton;

也有人提到了反射,確實反射可以不通過Instance創建實例,但反射的前提是需要一個可訪問的構造函數或靜態構造函數。如果我們的單例類的構造函數不是靜態的,那么會報異常:“No parameterless constructor defined for this object.”

所以,上面的兩種單例,只能通過調用Instance來加載,創建並使用。 

3.具體到不同業務,有可能會有通過反射,或者其他方式(比如單例中使用了本不該存在的靜態變量或靜態方法)使用單例類的情況,那么飢餓模式和懶加載模式就會出現差異了。

4.評論中有篇文章寫的不錯,大家可以學習一下,https://www.cnblogs.com/edisonchou/p/6618503.html,文中同樣提到了飢餓模式的不足,過早地創建實例,從而降低內存的使用效率,但如果我們的代碼是規范的,符合面向對象開發原則的話,是不會出現“過早創建實例”這種情況的,我們肯定是在需要的時候才會去創建實例,如果存在“過早創建實例”的情況發生,我們應該去考慮是否將不必要的功能移出單例,而不是將問題歸結於單例本身

 

 

測試驗證:

回家之后,自己做了實驗,證實兩種方式確實都是在類被調用的時候才會創建單例對象。

public static readonly 創建的單例

public class SingletonSimple
    {
        SingletonSimple()
        {
            Console.WriteLine($"Singleton Simple Create"); } public static readonly SingletonSimple Instance = new SingletonSimple(); public void Work() { Console.WriteLine("Singleton Simple Work"); } }

 

lock + 兩次判斷 創建的單例

public class SingletonLazy
    {
        SingletonLazy()
        {
            Console.WriteLine($"Singleton Lazy Create"); } static readonly object locker = new object(); static SingletonLazy singleton = null; public static SingletonLazy Instance { get { if (singleton == null) { lock (locker) { if (singleton == null) { singleton = new SingletonLazy(); } } } return singleton; } } public void Work() { Console.WriteLine("Singleton Lazy Work"); } }

 

main函數

    class Program
    {
        static void Main(string[] args) { Console.WriteLine("begin ..."); SingletonLazy.Instance.Work(); SingletonSimple.Instance.Work(); Console.WriteLine("end ..."); Console.Read(); } }

 

輸出結果如下 :

begin ...
Singleton Lazy Create
Singleton  Lazy Work
Singleton Simple Create
Singleton Simple Work
end ...

 

我們看,如果飢餓模式單例在程序啟動就自動加載的話,應該會先輸出“Singleton Simple Create”,但實際並不是這樣,並且我多次調整main函數中的單例調用順序,觀察結果,可以得出結論,兩種方式並沒有區別,都是在調用時加載的。

悔恨啊,居然栽在這么個小問題上,顏面掃地。

謹記:基礎原理,獨立思考,真的很重要。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM