設計模式之一:單例模式(Singleton Pattern)

寫這個系列的文章,只爲把所學的設計模式再系統的整理一遍。錯誤和不周到的地方歡迎你們批評。點擊這裏下載源代碼。html

何時使用單例模式

在程序運行時,某種類型只須要一個實例時,通常採用單例模式。爲何須要一個實例?第一,性能,第二,保持代碼簡潔,好比程序中經過某個配置類A讀取配置文件,若是在每處使用的地方都new A(),才能讀取配置項,一個是浪費系統資源(參考.NET垃圾回收機制),再者重複代碼太多。程序員

單例模式的實現

實現單例模式,方法很是多,這裏我把見過的方式都過一遍,來體會如何在支持併發訪問、性能、代碼簡潔程度等方面不斷追求極致。(單擊此處下載代碼)c#

實現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: }

執行結果以下:架構

SNAGHTML3ce09f

分析:併發

Singleton.Instance的get方法中建立instance並未考慮併發訪問的狀況,致使可能重複建立Singleton對象。下面的實現方法修復了此問題。app

實現2:簡單線程安全ide

要解決上面的問題,最簡單的方法就是在建立對象的時候加鎖。函數

   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: }

咱們再看看執行效果:

image

建立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: }

執行結果:

image

咱們先不討論結果,接着往下看看雙檢鎖方式的性能。

實現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: }

測試代碼和上面的同樣,結果以下:

image

性能提升了(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: }

執行結果以下:

image

把靜態構造函數去掉後執行結果以下:

image

這是由於沒有靜態構造函數的類,編譯時會被標記稱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: }

參考:

  1. Exploring the Singleton Design Pattern
  2. C#設計模式(7)-Singleton Pattern
  3. Implementing the Singleton Pattern in C#
  4. c#靜態構造函數
  5. C# and beforefieldinit
  6. 《研磨設計模式》
  7. 關於Type Initializer和 BeforeFieldInit的問題,看看你們可否給出正確的解釋
  8. [你必須知道的.NET]第二十三回:品味細節,深刻.NET的類型構造器
相關文章
相關標籤/搜索