去面試(對,又去面試)
問:單例模式瞭解吧,來,拿紙和筆寫一下單例模式。
我心想,這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函式中的單例呼叫順序,觀察結果,可以得出結論,兩種方式並沒有區別,都是在呼叫時載入的。
悔恨啊,居然栽在這麼個小問題上,顏面掃地。
謹記:基礎原理,獨立思考,真的很重要。