如何正确的使用单例模式
在最近的一個項目里面發現好多同事喜歡這樣運用單例模式,樣例代碼如下
public class Demo { public static Demo Instance{ get { return new Demo(); }} public string GetUserId(){ return "001";} public string GetUserName(){ return "tauruswu";} }在調用這個類的時候,是這樣操作的
var id = Demo.Instance.GetUserId(); var name = Demo.Instance.GetUserName();粗略一看,可能覺得沒有問題,最開始我也是這樣,看別人都這么寫,我也就這么寫,其實這個時候你的直覺已經明顯的欺騙你了,各位看官再仔細看看Demo類里面的靜態屬性Instance以及我們調用的方式,有沒有看出什么端倪來?
很顯然,上面的調用方法已經違背了單例模式的宗旨,或者可以說是披著單例模式的外衣,卻不做單例模式該做的事情。單例模式的解釋是:保證一個類僅有一個實例,并提供一個訪問它的全局訪問點。那么我們應該如何正確的使用單例模式了?
何為單例模式
再回頭看上面解釋單例模式的話,第一句話說“保證一個類僅有一個實例”,好,那么我們怎樣能夠保證一個類僅有一個實例了,幸好在C#里面,提供了私有構造器,我們在創建一個類的時候,往往會在類的構造函數里面初始化一些對象,這里的構造器是公開的,如下面
public Demo(){// to do }那么很顯然,私有構造器就是private的,如下面
private Demo(){// to do }一旦有了私有構造器,那么這個類就會阻止類外面的代碼創建實例,不相信我們就來嘗試一下。還是用上面的Demo類
public class Demo { private Demo(){ // to do } }然后再外面去實例化它,看截圖
?這樣一來,你應該明白了私有構造器的作用了吧。
解釋單例模式的前半句話上面說的很清楚了,然后在看看后半句“并提供一個訪問它的全局訪問點”,也就是說向外部提供訪問該實例的方法或者屬性,怎么寫?我們將開篇的例子稍作修改
public class Demo {private Demo(){ // to do } private static Demo _instance; public static Demo Instance{ get{if (_instance == null){
Console.WriteLine(string.Format("線程{0}在{1}時刻發現Instance為null", Thread.CurrentThread.Name,DateTime.Now));_instance = new Demo();
}return _instance;}} }
調用方式與開篇的一樣,這個時候你在單步調試進去,看看發現了什么。到這里,我們因該能正確的理解單例模式以及如何使用單例模式了。
多線程環境下莫名其妙的錯誤
上面的例子在單線程環境下可以正常的運轉,如果換做是多線程環境下,它還能正確的運轉嗎?
我們來做這樣一個實驗:1. 在一個程序啟動時創建兩個線程,線程A與線程B
2. 線程A與線程B分別調用Demo類
如果僅憑自覺的話,我們肯定會覺得只有一個線程來創建Demo的實例,那么事實是不是這樣了?Demo類還是上面的那個類,未作任何修改。然后在另一個類中啟動兩個線程,分別調用類Demo
public class Invoke{public void Run(){Thread t1 = new Thread(new ThreadStart(fun1));t1.Name = "AAA";t1.Start();Thread t2 = new Thread(new ThreadStart(fun2));t2.Name = "BBB";t2.Start();}private void fun1(){while (true){Demo1.Instance.GetUserId();Thread.Sleep(1);}}private void fun2(){while (true){Demo1.Instance.GetUserId();Thread.Sleep(1);}}}最后我們在控制臺程序里面運行調用類Singleton,看效果圖
哥,你目瞪口呆了吧?怎么會有這樣的結果?事實證明上面的寫法在多線程環境里面會出問題的,那么我們該怎么樣去修改它了,讓它能在多線程環境下正確的運行。
如何修正在多線程環境下的bug
這里我們會用到著名的雙檢鎖技術,英文名就是“Double-Check Locking”,它是線程同步機制中的一種,它背后的思路是,如果對象已經構造好,就不需要線程同步,另外如果調用如上面提到的屬性“Instance”的線程A發現對象沒有創建好,就會獲取一個線程同步鎖來確保只有一個線程構造單例對象,基于這,我們將Demo類再稍微調整下
public class Demo1{private Demo1(){// to do }private static Demo1 _lock = new Demo1();private static Demo1 _instance;public static Demo1 Instance{get{if (_instance != null) return _instance;Monitor.Enter(_lock);if (_instance == null){Console.WriteLine(string.Format("線程{0}在{1}時刻發現Instance為null", Thread.CurrentThread.Name, DateTime.Now));_instance = new Demo1();}Monitor.Exit(_lock);return _instance;}}}然后在調用類中再啟動多兩個線程CCC,DDD,再次啟動程序
這次的結果表明只有一個線程創建了Demo類的實例了。其實上面的寫法不是很嚴謹的,就是當私有構造器未執行完,其他的線程已經發現Instance不為null了,不過這個問題很難模擬出來。未了解決這種問題,那么就要用到Interlocked.Exchange() 這個方法。
還有其他方式創建單例嗎
除了雙檢索技術,還有其他方式實現單例模式嗎?答案是肯定的。先來看些下面這種方式
public class Demo2{private static Demo2 _demo2 = new Demo2();private Demo2(){Console.WriteLine(string.Format("線程{0}在{1}時刻執行私有構造函數", Thread.CurrentThread.Name, DateTime.Now));}public static Demo2 Instance{get { return _demo2; }}}?
在看下執行結果圖
?
那么它的原理是什么了?這里涉及到類型構造器了,由于當代碼首次訪問類的一個成員時,CLR 會自動調用一個類型的類構造器,所以當有一個線程訪問屬性Instance的時候,CLR會自動調用類構造器,從而創建這個對象的實例。
總結
這個話題已經被寫亂了,如果我之前不仔細看項目里面的代碼,我也不會發現這個問題,有些時候總是會被感覺所欺騙,所以最好的方法就是自己動手親自實踐一番,無非就是幾個小時的事情而已。那么你看完這篇文章之后有沒有什么感想了?
?
?
轉載于:https://www.cnblogs.com/wucj/p/3157294.html
總結
以上是生活随笔為你收集整理的如何正确的使用单例模式的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 看到南京饿死两个女竟的报道
- 下一篇: Oracle技术之OCRCONFIG工具
