說來慚愧,自己在畢業之前就該好好看看《劍指Offer》這本書的,但是各種原因就是沒看,也因此錯過了很多機會,后悔莫及。但是后悔是沒用的,現在趁還有余力,把這本書好好看一遍,並通過C#通通實現一遍,並記錄在我的博客中,作為學習筆記。
一、題目:實現Singleton模式
題目:設計一個類,我們只能生成該類的一個實例。
只能生成一個實例的類是實現了Singleton(單例)模式的類型。由於設計模式在面向對象程序設計中起着舉足輕重的作用,在面試過程中很多公司都喜歡問一些與設計模式相關的問題。在常用的模式中,Singleton是唯一一個能夠用短短幾十行代碼完整實現的模式。因此,寫一個Singleton的類型是一個很常見的面試題。
例如,在一個Flappy Bird游戲中,小鳥這個游戲對象在整個游戲中應該只存在一個實例,所有對於這個小鳥的操作(向上飛、向下掉等)都應該只會針對唯一的一個實例進行。
二、幾種不好的解法
2.1 不好的解法一:只適用於單線程環境
public sealed class Singleton1 { private Singleton1() { } private static Singleton1 instance = null; public static Singleton1 Instance { get { if(instance == null) { instance = new Singleton1(); } return instance; } } }
解法一的代碼在單線程的時候工作正常,但在多線程的情況下多個線程都會創建一個自己的實例,無法保證單例模式的要求。
2.2 不好的解法二:雖然在多線程環境中能工作但效率不高
public sealed class Singleton2 { private Singleton2() { } private static readonly object syncObject = new object(); private static Singleton2 instance = null; public static Singleton2 Instance { get { // 每個線程來之前先等待鎖 lock(syncObject) { if (instance == null) { instance = new Singleton2(); } } return instance; } } }
解法二就保證了我們在多線程環境中也只能得到一個實例,但是加鎖是一個非常耗時的操作,在沒有必要的時候我們應該盡量避免。
2.3 可行的解法三:加同步鎖前后兩次判斷實例是否已存在
前面講到的線程安全的實現方式的問題是要進行同步操作,那么我們是否可以降低通過操作的次數呢?其實我們只需在同步操作之前,添加判斷該實例是否為null就可以降低通過操作的次數了,這樣是經典的Double-Checked Locking方法,修改上面的屬性代碼如下:
public static Singleton3 Instance { get { // Double-Check 雙重判斷避免不必要的加鎖 if (instance == null) { // 確定實例為空時再等待加鎖 lock (syncObject) { // 確定加鎖后實例仍然未創建 if (instance == null) { instance = new Singleton3(); } } } return instance; } }
解法三用加鎖機制來確保在多線程環境下只創建一個實例,並且用兩個if判斷來提高效率。但是,這樣的代碼實現起來比較復雜,容易出錯。
三、兩種較好的解法
3.1 較好的解法一:利用靜態構造函數
C#的語法中有一個函數能夠確保只調用一次,那就是靜態構造函數。由於C#是在調用靜態構造函數時初始化靜態變量,.NET運行時(CLR)能夠確保只調用一次靜態構造函數,這樣我們就能夠保證只初始化一次instance。
public sealed class Singleton4 { private Singleton4() { } // 在大多數情況下,靜態初始化是在.NET中實現Singleton的首選方法。 static Singleton4() { } private static readonly Singleton4 instance = new Singleton4(); public static Singleton4 Instance { get { return instance; } } }
該解法是在 .NET 中實現 Singleton 的首選方法,但是,由於在C#中調用靜態構造函數的時機不是由程序員掌控的,而是當.NET運行時發現第一次使用該類型的時候自動調用該類型的靜態構造函數(也就是說在用到Singleton4時就會被創建,而不是用到Singleton4.Instance時),這樣會過早地創建實例,從而降低內存的使用效率。此外,靜態構造函數由 .NET Framework 負責執行初始化,我們對對實例化機制的控制權也相對較少。
3.2 較好的解法二:實現按需創建實例
public sealed class Singleton5 { private Singleton5() { } public static Singleton5 Instance { get { return Nested.instance; } } // 使用內部類+靜態構造函數實現延遲初始化 class Nested { static Nested() { } internal static readonly Singleton5 instance = new Singleton5(); } }
該解法在內部定義了一個私有類型Nested。當第一次用到這個嵌套類型的時候,會調用靜態構造函數創建Singleton5的實例instance。如果我們不調用屬性Singleton5.Instance,那么就不會觸發.NET運行時(CLR)調用Nested,也就不會創建實例,因此也就保證了按需創建實例(或延遲初始化)。
四、總結
在前面的5種實現單例模式的方法中:
第一種方法在多線程環境中不能正常工作,第二種模式雖然能在多線程環境中正常工作但時間效率很低,都不是面試官期待的解法。在第三種方法中我們通過兩次判斷一次加鎖確保在多線程環境能高效率地工作。
第四種方法利用C#的靜態構造函數的特性,確保只創建一個實例。第五種方法利用私有嵌套類型的特性,做到只在真正需要的時候才會創建實例,提高空間使用效率。