设计模式之一:单例模式(Singleton Pattern)
寫這個系列的文章,只為把所學的設計模式再系統的整理一遍。錯誤和不周到的地方歡迎大家批評。點擊這里下載源代碼。
什么時候使用單例模式
在程序運行時,某種類型只需要一個實例時,一般采用單例模式。為什么需要一個實例?第一,性能,第二,保持代碼簡潔,比如程序中通過某個配置類A讀取配置文件,如果在每處使用的地方都new A(),才能讀取配置項,一個是浪費系統資源(參考.NET垃圾回收機制),再者重復代碼太多。
單例模式的實現
實現單例模式,方法非常多,這里我把見過的方式都過一遍,來體會如何在支持并發訪問、性能、代碼簡潔程度等方面不斷追求極致。(單擊此處下載代碼)
實現1:非線程安全
1: using System; 2: using System.Collections.Generic; 3: using System.Linq; 4: using System.Text; 5: using System.Threading; 6: using System.Threading.Tasks; 7:? 8: namespace SingletonPatternNotTheadSafe 9: { 10: public sealed class Singleton 11: { 12: private static Singleton instance = null; 13:? 14: private Singleton() 15: { 16: } 17:? 18: public static Singleton Instance 19: { 20: get 21: { 22: if (instance == null) 23: { 24: Thread.Sleep(1000); 25: instance = new Singleton(); 26: Console.WriteLine(string.Format( 27: "[{0}]創建Singleton {1}" , Thread.CurrentThread.ManagedThreadId, instance.GetHashCode())); 28: } 29:? 30: Console.WriteLine(string.Format( 31: "[{0}]獲得Singleton {1}", Thread.CurrentThread.ManagedThreadId, instance.GetHashCode())); 32: return instance; 33: } 34: } 35: } 36: }為了能夠在下面的測試代碼中展示上面代碼的問題,這里在創建對象前,讓線程休息1秒,并且在控制臺打印出當前線程ID、對象的hashcode(一般不同對象的hashcode是不一樣的,但可能重復)。
1: using System; 2: using System.Collections.Generic; 3: using System.Linq; 4: using System.Text; 5: using System.Threading; 6: using System.Threading.Tasks; 7:? 8: namespace SingletonPatternNotTheadSafe 9: { 10: class Program 11: { 12: private static void Main(string[] args) 13: { 14: Thread t1 = new Thread(new ThreadStart(Compute)); 15:? 16: t1.Start(); 17:? 18: Compute(); 19:? 20: Console.ReadLine(); // 阻止主線程結束 21: } 22:? 23: private static void Compute() 24: { 25: Singleton o1 = Singleton.Instance; 26: } 27: } 28: }執行結果如下:
分析:
Singleton.Instance的get方法中創建instance并未考慮并發訪問的情況,導致可能重復創建Singleton對象。下面的實現方法修復了此問題。
實現2:簡單線程安全
要解決上面的問題,最簡單的方法就是在創建對象的時候加鎖。
1: using System; 2: using System.Collections.Generic; 3: using System.Linq; 4: using System.Text; 5: using System.Threading; 6: using System.Threading.Tasks; 7:? 8: namespace SingletonSimpleThreadSafe 9: { 10: public sealed class Singleton 11: { 12: private static Singleton instance = null; 13: private static readonly object _lock = new object(); 14:? 15: private Singleton() 16: { 17: } 18:? 19: public static Singleton Instance 20: { 21: get 22: { 23: lock (_lock) 24: { 25: if (instance == null) 26: { 27: Thread.Sleep(1000); 28: instance = new Singleton(); 29: Console.WriteLine(string.Format( 30: "[{0}]創建Singleton {1}", Thread.CurrentThread.ManagedThreadId, instance.GetHashCode())); 31: } 32: } 33:? 34: Console.WriteLine(string.Format( 35: "[{0}]獲得Singleton {1}", Thread.CurrentThread.ManagedThreadId, instance.GetHashCode())); 36: return instance; 37: } 38: } 39: } 40: }測試代碼如下:
1: using System; 2: using System.Collections.Generic; 3: using System.Linq; 4: using System.Text; 5: using System.Threading; 6: using System.Diagnostics; 7: using System.Threading.Tasks; 8:? 9: namespace SingletonSimpleThreadSafe 10: { 11: class Program 12: { 13: private static void Main(string[] args) 14: { 15: SingletonTest(); 16: } 17:? 18: private static void SingletonTest() 19: { 20: Thread t1 = new Thread(new ThreadStart(Compute)); 21:? 22: t1.Start(); 23:? 24: Compute(); 25:? 26: Console.ReadLine(); // 阻止主線程結束 27: } 28:? 29: private static void Compute() 30: { 31: Singleton o1 = Singleton.Instance; 32: } 33: } 34: }我們再看看執行效果:
創建Singleton只執行一次。但是這種寫法性能并不高,每次通過Singleton.Instance獲得實例對象都需要判斷鎖是否別別的線程占用。
這里我們修改一下Singleton,把代碼中的Thread.Sleep和Console.Writeline都去掉,這里我重新創建了一個Singleton2 class,兩個線程中循環調用100000000次,看一下這么實現的性能:
1: using System; 2: using System.Collections.Generic; 3: using System.Linq; 4: using System.Text; 5: using System.Threading; 6: using System.Threading.Tasks; 7:? 8: namespace SingletonSimpleThreadSafe 9: { 10: public sealed class Singleton2 11: { 12: private static Singleton2 instance = null; 13: private static readonly object _lock = new object(); 14:? 15: private Singleton2() 16: { 17: } 18:? 19: public static Singleton2 Instance 20: { 21: get 22: { 23: lock (_lock) 24: { 25: if (instance == null) 26: { 27: instance = new Singleton2(); 28: } 29: } 30:? 31: return instance; 32: } 33: } 34: } 35: }測試代碼如下:
1: using System; 2: using System.Collections.Generic; 3: using System.Linq; 4: using System.Text; 5: using System.Threading; 6: using System.Diagnostics; 7: using System.Threading.Tasks; 8:? 9: namespace SingletonSimpleThreadSafe 10: { 11: class Program 12: { 13: private static void Main(string[] args) 14: { 15: Singleton2Test(); 16: } 17:? 18: private static void Singleton2Test() 19: { 20: Thread t1 = new Thread(new ThreadStart(Compute2)); 21:? 22: t1.Start(); 23:? 24: Compute2(); 25:? 26: Console.ReadLine(); // 阻止主線程結束 27: } 28:? 29: private static void Compute2() 30: { 31: Stopwatch sw1 = new Stopwatch(); 32:? 33: sw1.Start(); 34:? 35: for (int i = 0; i < 100000000; i++) 36: { 37: Singleton2 instance = Singleton2.Instance; 38: } 39:? 40: sw1.Stop(); 41:? 42: Console.WriteLine(string.Format("[{0}]耗時:{1}毫秒", 43: Thread.CurrentThread.ManagedThreadId, 44: sw1.ElapsedMilliseconds)); 45: } 46: } 47: }執行結果:
我們先不討論結果,接著往下看看雙檢鎖方式的性能。
實現3:雙檢鎖實現的線程安全
Singleton雙檢鎖實現:
1: using System; 2: using System.Collections.Generic; 3: using System.Linq; 4: using System.Text; 5: using System.Threading; 6: using System.Threading.Tasks; 7:? 8: namespace SingletonDoubleCheckThreadSafe 9: { 10: public sealed class Singleton2 11: { 12: private static Singleton2 instance = null; 13: private static readonly object _lock = new object(); 14:? 15: private Singleton2() 16: { 17: } 18:? 19: public static Singleton2 Instance 20: { 21: get 22: { 23: if (instance == null) 24: { 25: lock (_lock) 26: { 27: if (instance == null) 28: { 29: instance = new Singleton2(); 30: } 31: } 32: } 33:? 34: return instance; 35: } 36: } 37: } 38: }測試代碼和上面的一樣,結果如下:
性能提高了(7571+7465-1410-1412)/ (7571+7465) * 100% = 81.2%。(實際項目中為了減少誤差,應該跑多遍測試得到多個結果的平均值和方差,這里為了方便,我只把一次測試結果貼出來。)
雙檢鎖機制在lock外又檢查了一次instance是否為null,這樣在第一次訪問使instance創建后,后面的調用都無需檢查lock是否被占用。
一名程序員要了解到這里算基本合格,如果想達到更高的水平,繼續往下看。這種方式有什么缺點呢?
- 上面的代碼在Java中不能正常工作。這是因為Java的Memory Model實現和.NET不一樣,并不保證一定在構造函數執行完成后才返回對象的引用。雖然Java 1.5版本重構了Memory Model,但是雙檢鎖機制在不給instance field加volatile關鍵字時,依然不能正常工作。
- Microsoft的.net memory model并不是按照標準的ECMA CLI規范實現,而是在標準上做了一些“增強”工作。MS .net CLR memory model中所有的寫操作都是VolatileWrite(參考《CLR via C#》第二版的第24章)。所以我們的代碼中不加volatile也能在IA64CPU 架構的機器上正常執行。但是如Jeffrey建議,最好還是遵循ECMA標準。
- 實現復雜。
實現4:非懶加載,無鎖實現線程安全
.NET中的static變量在class被第一次實例化的時候創建,且保證僅執行一次創建。利用這個特點,可以像如下實現:
1: using System; 2: using System.Collections.Generic; 3: using System.Linq; 4: using System.Text; 5: using System.Threading.Tasks; 6:? 7: namespace SingletonNotUsingLock 8: { 9: public class Singleton 10: { 11: private volatile static Singleton instance = new Singleton(); 12:? 13: // Explicit static constructor to tell C# compiler 14: // not to mark type as beforefieldinit 15: static Singleton() 16: { 17: Console.WriteLine("execute static constructor"); 18: } 19:? 20: private Singleton() 21: { 22: Console.WriteLine("execute private constructor"); 23: } 24:? 25: public static Singleton Instance 26: { 27: get 28: { 29: Console.WriteLine("instance get"); 30: return instance; 31: } 32: } 33: } 34: }上面的代碼可以更簡化一些,去掉Instance屬性,將私有的instance變量改成public的:
1: using System; 2: using System.Collections.Generic; 3: using System.Linq; 4: using System.Text; 5: using System.Threading.Tasks; 6:? 7: namespace SingletonNotUsingLock 8: { 9: public class Singleton2 10: { 11: public volatile static Singleton2 instance = new Singleton2(); 12:? 13: // Explicit static constructor to tell C# compiler 14: // not to mark type as beforefieldinit 15: static Singleton2() 16: { 17: Console.WriteLine("execute static constructor"); 18: } 19:? 20: private Singleton2() 21: { 22: Console.WriteLine("execute private constructor"); 23: } 24: } 25: }代碼非常簡潔。但是為什么有個靜態構造函數呢,我們看看下面的測試代碼:
1: using System; 2: using System.Collections.Generic; 3: using System.Linq; 4: using System.Text; 5: using System.Threading.Tasks; 6:? 7: namespace SingletonNotUsingLock 8: { 9: class Program 10: { 11: static void Main(string[] args) 12: { 13: Console.WriteLine("begin create singleton"); 14:? 15: Singleton s1 = Singleton.Instance; 16:? 17: Console.WriteLine("after create singleton"); 18:? 19: Singleton2 s2 = Singleton2.instance; 20:? 21: Console.WriteLine("after create singleton2"); 22: } 23: } 24: }執行結果如下:
把靜態構造函數去掉后執行結果如下:
這是因為沒有靜態構造函數的類,編譯時會被標記稱beforefieldinit,那么,beforefieldinit究竟表示什么樣的語義呢?Scott Allen對此進行了詳細的解釋:beforefieldinit為CLR提供了在任何時候執行.cctor的授權,只要該方法在第一次訪問類型的靜態字段之前執行即可。
實現5:無鎖懶加載
1: using System; 2: using System.Collections.Generic; 3: using System.Linq; 4: using System.Text; 5: using System.Threading.Tasks; 6:? 7: namespace SingletonNotUsingLockAndLazyLoad 8: { 9: public class Singleton 10: { 11: private Singleton() 12: { 13: Console.WriteLine("execute Singleton private constructor"); 14: } 15:? 16: public static Singleton Instance 17: { 18: 19: get 20: { 21: Console.WriteLine("execute Singleton.Instance get"); 22: return Nested.instance; 23: } 24: } 25:? 26: private class Nested 27: { 28: // Explicit static constructor to tell C# compiler 29: // not to mark type as beforefieldinit 30: static Nested() 31: { 32: Console.WriteLine("execute Nested static constructor"); 33: } 34:? 35: internal static readonly Singleton instance = new Singleton(); 36: } 37: } 38: }實現6:使用.NET 4.0中的Lazy<T>
1: using System; 2: using System.Collections.Generic; 3: using System.Linq; 4: using System.Text; 5: using System.Threading.Tasks; 6:? 7: namespace SingletonUsingLazyType 8: { 9: public sealed class Singleton 10: { 11: private static readonly Lazy<Singleton> lazy = 12: new Lazy<Singleton>(() => new Singleton()); 13:? 14: public static Singleton Instance { get { return lazy.Value; } } 15:? 16: private Singleton() 17: { 18: } 19: } 20: }參考:
轉載于:https://www.cnblogs.com/EthanCai/p/3150595.html
總結
以上是生活随笔為你收集整理的设计模式之一:单例模式(Singleton Pattern)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows打印机驱动开发笔记(一)
- 下一篇: 好久没写了,重装了系统重新配置的Live