【转】面试:一个单例模式,足以把你秒成渣
去面試(對,又去面試)
問:單例模式了解吧,來,拿紙和筆寫一下單例模式。
我心想,這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聲明是靜態的。
| 1 | 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函數中的單例調用順序,觀察結果,可以得出結論,兩種方式并沒有區別,都是在調用時加載的。
悔恨啊,居然栽在這么個小問題上,顏面掃地。
?
上一篇,一個簡單的單例模式,引起了不少人關注,確實沒想到。
首先,重申一下,這只是作為自己的一個面試記錄,以及思考過程,主要是意識到自己之前在對待靜態變量上的一個錯誤(靜態類,或者帶靜態構造函數,靜態字段,靜態方法的類,并不會在項目啟動時就實例化,涉及到對象創建,內存分配,需要好好思考),再有,自己的一些術語上可能不太規范,可能導致大家。
總結一下:
一,關于饑餓模式和懶加載模式的區別的標準答案:“饑餓模式是在類加載時創建實例,而懶加載模式是在Instance被調用時創建實例。”
二,關于單例模式的規范:單例模式的所有操作,都應該通過Instance調用實現(對外不能提供其他靜態字段或靜態方法)。這個規范是我自己定的,大家見仁見智。
三,關于效率問題:如果是符合上述規范的,那么就不存在"過早地創建實例,從而降低內存的使用效率"的問題(真想不到什么場景下會提前創建一個單例而不使用)。
四,關于volatile:好幾個人提到了volatile,從微軟文檔上我們可以得知,這個關鍵字標記一個可能被多個線程同時修改的字段,當這個字段值被修改后,其他線程中的字段值也都會即時發生變化。
這段文檔看的人一頭霧水,直到有人在評論中提出“指令重排”,頓時豁然開朗,非常感謝。
指令重排是由于編譯器,運行時,或者硬件,為提高代碼運行效率,會在保證最終執行結果不變的情況下,改變代碼的執行順序。
這是一種優化方案,在單線程下沒有問題,但在多線程的情況下,由于不同線程內的代碼執行順序改變,可能產生錯誤。
怎么證明指令重排確實發生了呢,這篇文章中給了示例,大家可以試一下,加深印象。https://developer.aliyun.com/article/765749
關于指令重排和volatile,這篇文章解釋的很好,https://www.sohu.com/a/211287207_684445,大家可以學習一下。
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的【转】面试:一个单例模式,足以把你秒成渣的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 吉利拟收购魅族上热搜 网友泪目:终于成大
- 下一篇: 【转】Web Reference和Ser