概述
單例模式大概是23種設計模式里面用的最多,也用的最普遍的了,也是很多很多人一問設計模式都有哪些必答的第一種了;我們先復習一下餓漢式和懶漢式的單例模式,再談其創建方式會帶來什么問題,并一一解決!還是老規矩,先上代碼,不上代碼,紙上談兵咱把握不住,
餓漢式代碼
public class SingleHungry { private readonly static SingleHungry _singleHungry = new SingleHungry(); private SingleHungry() { } public static SingleHungry GetSingleHungry() { return _singleHungry; } }
代碼很簡單,意思也很明確,接著我們寫點代碼測驗驗證一下;
第一種測驗: 建構式私有的,new的時候報錯,因為我們的建構式是私有的,
SingleHungry _singleHungry=new SingleHungry();
第二種測驗: 比對創建多個物件,然后多個物件的Hashvalue
public class SingleHungryTest { public static void FactTestHashCodeIsSame() { Console.WriteLine("單例模式.餓漢式測驗!"); var single1 = SingleHungry.GetSingleHungry(); var single2 = SingleHungry.GetSingleHungry(); var single3 = SingleHungry.GetSingleHungry(); Console.WriteLine(single1.GetHashCode()); Console.WriteLine(single2.GetHashCode()); Console.WriteLine(single3.GetHashCode()); } }
測驗下來,三個物件的hash值是一樣的,如下圖:

餓漢式結論總結
餓漢式的單例模式不推薦使用,因為還沒呼叫,物件就已經創建,造成資源的浪費;
懶漢式代碼
public class SingleLayMan { //1、私有化建構式 private SingleLayMan() { } //2、宣告靜態欄位 存盤我們唯一的物件實體 private static SingleLayMan _singleLayMan; //通過方法 創建實體并回傳 public static SingleLayMan GetSingleLayMan1() { //這種方式不可用 會創建多個物件,謹記 return _singleLayMan = new SingleLayMan(); } /// <summary> ///懶漢式單例模式只有在呼叫方法時才會去創建,不會造成資源的浪費 /// </summary> /// <returns></returns> public static SingleLayMan GetSingleLayMan2() { if (_singleLayMan == null) { Console.WriteLine("我被創建了一次!"); _singleLayMan = new SingleLayMan(); } return _singleLayMan; } }
測驗代碼
public class SingleLayManTest { /// <summary> /// 會創建多個物件.hash值不一樣 /// </summary> public static void FactTest() { Console.WriteLine("單例模式.懶漢式測驗!"); var singleLayMan1 = SingleLayMan.GetSingleLayMan1(); var singleLayMan2 = SingleLayMan.GetSingleLayMan1(); Console.WriteLine(singleLayMan1.GetHashCode()); Console.WriteLine(singleLayMan2.GetHashCode()); } /// <summary> /// 單例模式.懶漢式測驗:懶漢式單例模式只有在呼叫方法時才會去創建,不會造成資源的浪費,但會有執行緒安全問題 /// </summary> public static void FactTest1() { Console.WriteLine("單例模式.懶漢式測驗!"); var singleLayMan1 = SingleLayMan.GetSingleLayMan2(); var singleLayMan2 = SingleLayMan.GetSingleLayMan2(); Console.WriteLine(singleLayMan1.GetHashCode()); Console.WriteLine(singleLayMan2.GetHashCode()); } /// <summary> /// 單例模式.懶漢式多執行緒環境測驗! /// </summary> public static void FactTest2() { Console.WriteLine("單例模式.懶漢式多執行緒環境測驗!"); for (int i = 0; i < 10; i++) { new Thread(() => { SingleLayMan.GetSingleLayMan2(); }).Start(); } //Parallel.For(0, 10, d => { // SingleLayMan.GetSingleLayMan2(); //}); } }
懶漢式結論總結
懶漢式的代碼如上已經概述,上面GetSingleLayMan1()會創建多個物件,這個沒什么好說的,肯定不推薦使用;GetSingleLayMan2()是大多數人經常使用的,可解決剛才因為餓漢式創建帶來的缺點,但也帶來了多執行緒的問題,如果不考慮多執行緒,那是夠用了,
話說回來,既然剛才餓漢式和懶漢式各有其優缺點,那我們該如何抉擇呢?到底選擇哪一種?
其它方式創建單例—餓漢式+靜態內部類
public class SingleHungry2 { public static SingleHungry2 GetSingleHungry() { return InnerClass._singleHungry; } public static class InnerClass { public readonly static SingleHungry2 _singleHungry = new SingleHungry2(); } }
這個代碼,用了餓漢式結合靜態內部類來創建單例,執行緒也安全,不失為創建單例的一種辦法,
其它方式創建單例—懶漢式+反射
首先我們解決一下剛才懶漢式創建單例的執行緒安全問題,上代碼:
/// <summary> /// 通過反射破壞創建物件 /// </summary> public class SingleLayMan1 { //私有化建構式 private SingleLayMan1() { } //2、宣告靜態欄位 存盤我們唯一的物件實體 private static SingleLayMan1? _singleLayMan; private static object _oj = new object();
/// <summary> /// //解決多執行緒安全問題,雙重鎖定,減少系統消耗,節約資源 /// </summary> public static SingleLayMan1 GetSingleLayMan() { if (_singleLayMan == null) { lock (_oj) { if (_singleLayMan == null) { _singleLayMan = new SingleLayMan1(); Console.WriteLine("我被創建了一次!"); } } } return _singleLayMan; } }
具體描述,在代碼里面已經說得足夠清楚,一看肯定明白,我們還是寫點測驗代碼,驗證一下,上代碼:
public class SingleLayManTest1 { public static void FactTestReflection() { var singleLayMan1= SingleLayMan1.GetSingleLayMan(); var type = Type.GetType("_01單例模式.反射破壞單例模式.SingleLayMan1"); //獲取私有的建構式 var ctors = type?.GetConstructors(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); //執行建構式 SingleLayMan1 singleLayMan = (SingleLayMan1)ctors[0].Invoke(null); Console.WriteLine(singleLayMan1.GetHashCode()); Console.WriteLine(singleLayMan.GetHashCode()); } }
上面的代碼分別通過SingleLayMan1.GetSingleLayMan2()和反射創建物件,輸出二者物件hash值比較,結果肯定是不一樣的,重點是我們可以通過反射創建物件,
通過上面的代碼,不知道大家有沒有意識到我們雖通過加鎖解決了執行緒安全問題,但仍會出現問題;正常創建物件的順序是:
1、new 在記憶體中開辟空間
2、 執行建構式 創建物件
3、 把空間指向我們的對像
但如果因為我們的程式使用多執行緒,則會發生"指令重排",本應執行順序為1、2、3,實際執行順序為1、3、2,但這種情況很少,不過我們寫程式嘛,肯定追求嚴謹一點準沒錯,
如果需要解決該問題需要給定義的私有區域變數加關鍵字 加上volatile (意思不穩定的 ,可變的) ,加該關鍵字可以避免指令重排,具體代碼主要是這句如下:
private volatile static SingleLayMan? _singleLayMan;
到這里,大家認為還有沒有問題?答案是肯定的,不然我就不會寫這篇文章了,通過反射既然可以創建物件,那么我們寫的創建實體代碼還有什么意義,有沒有什么辦法避免反射創建物件呢?
如果認真看了之前的反射創建物件代碼,肯定發現反射是通過建構式來創建物件的,那么我們相應的就在建構式處理一下,來,我們繼續上代碼:
/// <summary> /// 解決反射創建物件的問題 /// </summary> public class SingleLayMan3 { //2、宣告靜態欄位 存盤我們唯一的物件實體 private volatile static SingleLayMan3? _singleLayMan; private static object _oj = new object(); //私有化建構式 private SingleLayMan3() { lock (_oj) { if (_singleLayMan != null) { throw new Exception("不要通過反射來創建對像!"); } } } /// <summary> /// //解決多執行緒安全問題,雙重鎖定,減少系統消耗,節約資源 /// </summary> public static SingleLayMan3 GetSingleLayMan() { if (_singleLayMan == null) { lock (_oj) { if (_singleLayMan == null) { _singleLayMan = new SingleLayMan3(); Console.WriteLine("我被創建了一次!"); } } } return _singleLayMan; } }
下面繼續上測驗代碼,驗證一下:
public class SingleLayManTest3 { /// <summary> /// 第一次通過呼叫 SingleLayMan3.GetSingleLayMan()創建物件導致_singleLayMan不為空,之后再去通過反射創建物件時,建構式里面判斷創建物件導致_singleLayMan變數,報例外 /// </summary> public static void FactTestReflection() { var singleLayMan1= SingleLayMan3.GetSingleLayMan(); var type = Type.GetType("_01單例模式.反射破壞單例模式.SingleLayMan3"); //獲取私有的建構式 var ctors = type?.GetConstructors(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); //執行建構式 SingleLayMan3 singleLayMan = (SingleLayMan3)ctors[0].Invoke(null); Console.WriteLine(singleLayMan1.GetHashCode()); Console.WriteLine(singleLayMan.GetHashCode()); } }
結論其實測驗方法已經說明:第一次通過呼叫 SingleLayMan3.GetSingleLayMan()創建物件導致_singleLayMan不為空,之后再去通過反射創建物件時,建構式里面判斷創建物件導致_singleLayMan變數,報例外,
其實到這里,有人肯定發現了問題,第一次通過去執行自己寫的創建單例方法來創建物件,后面再執行反射時才會報例外,那有沒有什么辦法,只要有人第一次反射創建物件時就報例外呢?
定義區域變數解決反射創建物件問題
public class SingleLayMan4 { //2、宣告靜態欄位 存盤我們唯一的物件實體 private volatile static SingleLayMan4? _singleLayMan; private static object _oj = new object(); private static bool _isOk = false; //私有化建構式 private SingleLayMan4() { lock (_oj) { if (_isOk == false) { _isOk = true; } else { throw new Exception("不要通過反射來創建對像!只有第一次通過反射創建物件會成功!請做第一個吃葡萄的人!"); } } } /// <summary> /// //解決多執行緒安全問題,雙重鎖定,減少系統消耗,節約資源 /// </summary> public static SingleLayMan4 GetSingleLayMan() { if (_singleLayMan == null) { lock (_oj) { if (_singleLayMan == null) { _singleLayMan = new SingleLayMan4(); Console.WriteLine("我被創建了一次!"); } } } return _singleLayMan; } }
測驗代碼,驗證一下:
public static void FactTestReflection() { //第一次創建物件會成功 var singleLayMan1 = GetReflectionSingleLayMan4Instance(); //第二次創建物件會失敗,報例外 var singleLayMan2 = GetReflectionSingleLayMan4Instance(); Console.WriteLine(singleLayMan1.GetHashCode()); } private static SingleLayMan4 GetReflectionSingleLayMan4Instance() { var type = Type.GetType("_01單例模式.反射破壞單例模式.SingleLayMan4"); //獲取私有的建構式 var ctors = type?.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic); //執行建構式 SingleLayMan4 singleLayMan = (SingleLayMan4)ctors[0].Invoke(null); return singleLayMan; }
第一次創建物件會成功,因為執行建構式時沒有執行GetSingleLayMan(),跨過了new,導致_isOk賦值true,第二次反射創建執行建構式時判斷變數_isOk為true,走入例外邏輯,
但這樣做真的就安全了嗎?既然可以通過反射執行建構式來創建物件,那也可以通過反射改變區域變數_isOk 的值,上代碼:
/// <summary> /// 通過反射也可以改變區域變數_isOk的值,繼續創建物件 /// </summary> public static void FactTestReflection2() { Type type = Type.GetType("_01單例模式.反射破壞單例模式.SingleLayMan4"); //獲取私有的建構式 var ctors = type?.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic); //執行建構式 SingleLayMan4 singleLayMan1 = (SingleLayMan4)ctors[0].Invoke(null); FieldInfo fieldInfo = type.GetField("_isOk", BindingFlags.NonPublic | BindingFlags.Static); fieldInfo.SetValue("_isOk", false); SingleLayMan4 singleLayMan2 = (SingleLayMan4)ctors[0].Invoke(null); Console.WriteLine(singleLayMan1.GetHashCode()); Console.WriteLine(singleLayMan2.GetHashCode()); }
最后
大家或許發現了,只要有反射存在,哪怕你的邏輯寫的再嚴謹,它仍然可以反射創建物件,只因為它是反射!所以,單例模式的安全性也是相對而言的,具體選擇用哪個,取決專案的業務場景了,如有發現問題,歡迎不吝賜教!
原始碼地址:https://gitee.com/mhg/design-mode-demo.git
作者:課間一起牛
出處:https://www.cnblogs.com/mhg215/
聲援博主:如果您覺得文章對您有幫助,請點擊文章末尾的【關注我】吧!
別忘記點擊文章右下角的【推薦】支持一波,~~~///(^v^)\\\~~~ .
本文著作權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利,
如果您有其他問題,也歡迎關注我下方的公眾號,可以聯系我一起交流切磋!
碼云:碼云 github:github
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/500900.html
標籤:C#
