一、前言
這篇文章是學習單例模式的第二篇,之前的文章一下子就給出來看起來很高大上的實現方法,但是這種模式還是存在漏洞的,具體有什么問題,大家可以停頓一會兒,思考一下。好了,不賣關子了,下面我們來看看每種單例模式存在的問題以及解決辦法。
二、每種Singleton 模式的演進
- 模式一
public class LazySingleton { private static LazySingleton lazySingleton = null; private LazySingleton() { } public static LazySingleton GetInstance() { if (lazySingleton == null) { lazySingleton = new LazySingleton(); } return lazySingleton; } }
問題:該模式下在多線程下就會存在問題,因為你不知道線程執行的先后順序,不信看下面的調試,如下。


我們現在讓線程Two執行,它會進入到if里面,因為線程one已經被凍結,調試結果:

接着,我們把凍結的線程one解凍,執行完成的結果如下:

發現,竟然產生了兩個實例,這也就說明了上面實現單例模式在多線程下確實存在問題,為了解決在多線程的問題,引出了下面的單例模式。
- 模式二:DoubleCheck雙重檢查

問題:上面的代碼已經加上了lock,可以解決多線程的問題,但是這樣還是會出現問題,出現問題的地方在上面的兩處斷點處。多線程在多核CPU上執行時寄存器緩存和指令的重新排序【也就是new關鍵字步驟2和步驟3交換】雖然出現的概率很小,但是這種隱患一定要消除。如果出現指令重排的話,一個線程還沒來得及把分配對象的指針復制給變量lazySingleton,另外一個線程就會進入到第一個斷點的if邏輯里面。下面分別貼出寄存器緩存和指令重新排序的示意圖:
緩存數據示意圖:

(注意:圖片來源自https://theburningmonk.com/2010/03/threading-understanding-the-volatile-modifier-in-csharp/)
現代計算機中的內存很復雜,有多級緩存,處理器寄存器和多個處理器共享主內存等。處理器可能會從主內存中讀取數據緩存到寄存器中,另一個線程可能會使用緩存的數據,並且如果修改僅更新主內存,再次期間並發運行在另外一個CPU上的線程,可能讀取的還是之前的值。 在此期間,在另一個CPU上並發運行的另一個線程可能已經從主存儲器中讀取了相同的數據位並使用了過時的數據版本。
指令重排示意圖(下面的示意圖來自:geely老師的Java設計模式課程):

對於單線程來說既是指令重排也不會影響,但是對於多線程就會有影響,如下圖所示:

為了解決上面的問題有兩種做法:1)不允許2和3進行指令重排序。2)允許線程0可以重排序但是不允許線程1重排序。
對於解決辦法1:可以使用volatile關鍵字,它可以禁止重排序以及緩存的問題。
對於解決辦法2:靜態內部類-基於類初始化的延遲加。
- 模式三:解決辦法1示例代碼:

- 模式四:解決辦法2示例代碼:
public class StaticInnerClassSingleton { private static class InnerClass { internal static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton(); } public static StaticInnerClassSingleton GetInstance() { return InnerClass.staticInnerClassSingleton; } }
static void GetInstancev5() { var hashCode = StaticInnerClassSingleton.GetInstance().GetHashCode(); Console.WriteLine(hashCode); }
for (int i = 0; i < 10; i++) { Thread thread = new Thread(GetInstancev5); thread.Start(); if (i%2==0) { Thread.Sleep(1000); } }
驗證結果:

模式五:餓漢模式
public class CurrentSingleton { private static CurrentSingleton uniqueInstance = new CurrentSingleton(); private CurrentSingleton() { } public static CurrentSingleton Instance { get { return uniqueInstance; } } }
聊到這里,關於單例模式的幾種模式已經差不多了,該聊的已經聊完了,大多小伙伴們可能就了解到這里就結束了,先舒口氣,再繼續往下看,你會有意向不到的收獲。
三、單例模式下的問題解決辦法
- 問題一:反射攻擊單例模式三
單例模式三(懶漢模式)代碼:
public class LazyDoubleCheckSingleton { private volatile static LazyDoubleCheckSingleton lazySingleton = null; private static readonly object _threadSafetyLock = new object(); private LazyDoubleCheckSingleton(){} public static LazyDoubleCheckSingleton GetInstance() { if (lazySingleton == null) { lock(_threadSafetyLock) { if (lazySingleton == null) { //注意:new關鍵字做了下面三步的工作: //1、分配內存給這個對象 //2、初始化對象 //3、設置lazySingleton指向剛分配的內存地址 lazySingleton = new LazyDoubleCheckSingleton(); } } } return lazySingleton; } }

看到沒,我們通過反射也可以創建類的實例,那怕你的構造函數是private的,我通過反射都可以來創建對象的實例。同理你可以嘗試使用該方法來攻擊模式五(餓漢模式)。
那我們該如何防御?對於餓漢模式、基於靜態類模式的單例,我們可以通過下面的方法來防御:
在對應的private構造函數中添加一下代碼:


對於懶漢模式的單例這種方法還適用嗎?不一定,請看下面的代碼:
基於模式三【見上】的代碼修改:


驗證結果:

發現該方式處理不起作用。對於這個問題我們該怎么解決?嘗試的方法如下:
public class LazyDoubleCheckSingleton { private volatile static LazyDoubleCheckSingleton lazySingleton = null; private static readonly object _threadSafetyLock = new object(); private static bool flag = true; private LazyDoubleCheckSingleton(){ if (flag) { flag = false; } else { throw new Exception("單例構造器進制反射調用"); } } public static LazyDoubleCheckSingleton GetInstance() { if (lazySingleton == null) { lock(_threadSafetyLock) { if (lazySingleton == null) { //注意:new關鍵字做了下面三步的工作: //1、分配內存給這個對象 //2、初始化對象 //3、設置lazySingleton指向剛分配的內存地址 lazySingleton = new LazyDoubleCheckSingleton(); } } } return lazySingleton; } }
Type type = typeof(LazyDoubleCheckSingleton); object sobj = Activator.CreateInstance(type, true); Console.WriteLine(LazyDoubleCheckSingleton.GetInstance().GetHashCode()); Console.WriteLine(sobj.GetHashCode());
驗證結果:

這種方法看似解決了懶漢模式的問題,但是!它真的能解決這個問題嗎?大家可以想一下,為什么解決不了?我也就不賣關子了,原因就是反射,反射的威力太強了,上面演示的,即使你的構造函數是private我也能創建對象,區區一個字段,反射修改你的值不是很輕松嗎。
反射攻擊演示:

所以懶漢模式的單例,是防御不了反射攻擊的,至於Java中有一個叫枚舉模式的單例,可以解決這個問題,至於C#目前我還沒想出好的解決辦法,如果大家有好的解決辦法可以貢獻到評論區。好了問題一講到這里已經差不多了,下面我們來介紹問題二。
- 問題:序列化破壞單例模式
背景:在某些場景下我們需要把類序列化到文件當中,正好這個類是單例的,正常的情況應該是:序列化到文件中,再從文件反序列化,應該是同一個類,但一般的處理方法真的能得到同一個類嗎?
實例代碼:
[Serializable]
public class StaticInnerClassSingleton { private static class InnerClass { internal static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton(); } public static StaticInnerClassSingleton GetInstance() { return InnerClass.staticInnerClassSingleton; } }
//序列化到文件: var obj = StaticInnerClassSingleton.GetInstance(); var formatter = new BinaryFormatter(); var stream = new FileStream("D:\\Example.txt", FileMode.Create, FileAccess.Write); formatter.Serialize(stream, obj); stream.Close(); //從文件讀取出來反序列化 stream = new FileStream("D:\\Example.txt", FileMode.Open, FileAccess.Read); var obj2 = (StaticInnerClassSingleton)formatter.Deserialize(stream); Console.WriteLine(obj.GetHashCode()); Console.WriteLine(obj2.GetHashCode());
驗證結果:

看到沒,竟然是兩個不同的實例,如果大家遇到這樣的場景可以使用下面的方法來保障反序列化出來的是同一個對象,我們只需要修改單例模式的類。代碼如下:
[Serializable] public class StaticInnerClassSingleton: ISerializable { private StaticInnerClassSingleton() { } private static class InnerClass { internal static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton(); } public static StaticInnerClassSingleton GetInstance() { return InnerClass.staticInnerClassSingleton; } public void GetObjectData(SerializationInfo info, StreamingContext context) { info.SetType(typeof(SingletonHelper)); } [Serializable] private class SingletonHelper : IObjectReference { public object GetRealObject(StreamingContext context) { return InnerClass.staticInnerClassSingleton; } } }
如果想知道為什么要這樣寫我就不在解釋了,大家可以參考這篇文章:http://geekswithblogs.net/maziar/archive/2012/07/19/serializing-singleton-objects-c.aspx 好了講到這里基本上單例這種設計模式,你已經掌握的非常好了,希望對你有幫助,謝謝,如果覺得不錯的話,可以推薦一下。之前一直想寫這個系列的博客,希望把自己平時學的和工作中的經驗分享出來,共同進步,這個系列的標題是“從源碼中學習設計模式
這里的源碼主要就是ASP.Net Core2.1的源碼,現在.Net Core 3.0已經是預覽版,還沒有正式版,也希望.Net Core 越來越好。也希望我的文章能對你有幫助。
四、總結
單例這種設計模式,具體使用哪種要看你的使用場景,並不是那種模式一定就好,這是需要權衡的,希望看完本篇文章,你在使用該模式能得心應手。另外大家不要和依賴注入中的單例混淆,之前再介紹依賴注入最佳實踐的文章中有園友就混淆了。
參考資料:
geely老師的《Java設計模式精講》
作者:郭崢
出處:http://www.cnblogs.com/runningsmallguo/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。
