單件模式(C#)
2018-07-10 10:16
【原創】
本文地址:https://www.cnblogs.com/qiaoge0923/p/9282845.html
github示例代碼地址:https://github.com/JoySino/DesignPatternStudy/tree/master/ConsoleDemo/ConsoleDemo/Singleton
1.前言
單件模式保證一個類只有唯一一個實例,並提供一個全局訪問點。《設計模式——可復用面向對象軟件的基礎》中是這樣定義的:
類自身保存了該類的實例對象(自己創建實例),通過截取創建新實例的請求保證沒有其他新實例被創建(保證實例唯一),並且提供一個全局訪問該實例對象的方法(對外提供唯一訪問點),這就是Singleton模式。
常用的場景如:
系統日志記錄器,系統配置管理器,模態窗口等等。
單件模式也允許子類繼承,目前未接觸到實際應用,暫無具體了解,所以下面的介紹均不考慮 子類擴展的情況。
2.普通懶漢單例模式
2.1代碼

1 /// <summary> 2 /// 多線程安全單例模式示例 3 /// </summary> 4 public sealed class LockSingleton 5 { 6 #region 單例 7 8 private static LockSingleton _instance; 9 /// <summary> 10 /// 私有化構造函數,使得類不可通過new來創建實例 11 /// </summary> 12 private LockSingleton() { } 13 14 //寫法1:通過屬性獲取實例 15 public static LockSingleton Instance 16 { 17 get 18 { 19 if (_instance == null) 20 { 21 _instance = new LockSingleton(); 22 } 23 return _instance; 24 } 25 } 26 27 //寫法2:通過方法獲取實例 28 public static LockSingleton GetInstance() 29 { 30 //這里可以copy屬性Instance中的代碼 31 return Instance; 32 } 33 #endregion 34 }
2.2說明
這種寫法在多線程環境中會存在安全問題,兩個線程同時訪問時,都檢測到單例對象為空,所以都會執行初始化操作,這樣就會創建兩個實例對象來,無法做到唯一,所以一般不建議如此寫。
3.多線程安全單例模式
3.1代碼

1 /// <summary> 2 /// 多線程安全單例模式示例 3 /// </summary> 4 public sealed class LockSingleton 5 { 6 #region 字段 7 8 public int normalX = SetValue(staticX); 9 10 public static int staticX = 1; 11 12 #endregion 13 14 #region 單例 15 16 private static LockSingleton _instance; 17 private static readonly object lockObj = new object(); 18 /// <summary> 19 /// 私有化構造函數,使得類不可通過new來創建實例 20 /// </summary> 21 private LockSingleton() 22 { 23 Console.WriteLine($"normalX={normalX}"); 24 Console.WriteLine($"staticX={staticX}"); 25 Console.WriteLine($"normalY={normalY}"); 26 Console.WriteLine($"staticY={staticY}"); 27 } 28 29 //寫法1:通過屬性獲取實例 30 public static LockSingleton Instance 31 { 32 get 33 { 34 if (_instance == null) 35 { 36 lock (lockObj) 37 { 38 if (_instance == null) 39 { 40 _instance = new LockSingleton(); 41 } 42 } 43 } 44 return _instance; 45 } 46 } 47 48 //寫法2:通過方法獲取實例 49 public static LockSingleton GetInstance() 50 { 51 //這里可以copy屬性Instance中的代碼 52 return Instance; 53 } 54 #endregion 55 56 #region 其他 57 58 public int normalY = SetValue(staticY); 59 60 public static int staticY = 1; 61 62 private static int SetValue(int val) 63 { 64 return val; 65 } 66 67 #endregion 68 }
3.2調用過程

1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Console.WriteLine("程序開始"); 6 7 Singleton.LockSingleton.GetInstance(); 8 9 Console.ReadKey(); 10 } 11 }
3.3運行效果
3.4說明
normalX、staticX、normalY、staticY和Instance的執行順序為:外部調用》staticX 》staticY》單例對象_instance》normalX》normalY》私有構造函數。
使用懶漢式單例模式時必須要考慮多線程環境下的線程安全問題,本例中直接使用了線程安全的單例模式,鎖前鎖后雙重判斷,可以有效避免多線程同時讀到空單例對象可能導致創建多個實例對象的問題。
4.靜態初始化單例類示例1
4.1代碼

1 /// <summary> 2 /// 餓漢式單例類,靜態初始化單例類示例1 3 /// </summary> 4 public sealed class ReadonlySingleton 5 { 6 #region 字段 7 8 public int normalX = SetValue(staticX); 9 10 public static int staticX = 1; 11 12 #endregion 13 14 #region 單例 15 /// <summary> 16 /// 靜態初始化單例,只有通過這種寫法並且調用這個字段方能是餓漢式單例 17 /// </summary> 18 public static readonly ReadonlySingleton Instance = new ReadonlySingleton(); 19 20 /// <summary> 21 /// 私有化構造函數,使得類不可通過new來創建實例 22 /// </summary> 23 private ReadonlySingleton() 24 { 25 Console.WriteLine($"normalX={normalX}"); 26 Console.WriteLine($"staticX={staticX}"); 27 Console.WriteLine($"normalY={normalY}"); 28 Console.WriteLine($"staticY={staticY}"); 29 } 30 31 #endregion 32 33 #region 其他 34 35 public int normalY = SetValue(staticY); 36 37 public static int staticY = 1; 38 39 public void ShowLog() 40 { 41 Console.WriteLine($"{Common.TimeHelper.PrintMillisecond()}:ReadonlySingleton ShowLog"); 42 } 43 44 private static int SetValue(int val) 45 { 46 return val; 47 } 48 49 #endregion 50 }
4.2調用過程

1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Console.WriteLine("程序開始"); 6 7 var tmp = Singleton.ReadonlySingleton.Instance; 8 9 Console.ReadKey(); 10 } 11 }
4.3運行效果
4.4說明
餓漢式單例類,在類加載的時候就會創建實例對象並一直存在於內存當中直至程序退出,所以,在Main函數的第一句執行前就已經執行了單例類的實例化。
normalX、staticX、normalY、staticY和Instance的執行順序為:staticX 》Instance》normalX》normalY》構造函數》staticY》外部調用。
可以看出,對於靜態字段,定義在單例字段前會先初始化,並在構造函數調用前執行,而定義在單例字段后會后初始化,並且在構造函數調用后執行;對於無修飾符的字段,無論位置在哪,都會在單例字段初始化后按順序初始化,並且在構造函數調用前執行。所以,normalY、staticY都打印了0而不是1。
單例字段Instance是公有的,可以外部訪問,只會實例化一次。這樣寫的單例類其字段會在類第一次使用前就初始化(具體時機由CLR決定),進而提前占用系統資源,如果想要在第一次訪問時再實例化對象,那么只需要把單例字段變成私有的,並提供外部訪問接口即可,或者定義內部類來實例化單例字段,詳情見靜態初始化單例類示例2、3、4。
5.靜態初始化單例類示例2(單例私有化)
5.1代碼

1 public sealed class ReadonlySingleton2 2 { 3 #region 字段 4 5 public int normalX = SetValue(staticX); 6 7 public static int staticX = 1; 8 9 #endregion 10 11 #region 單例 12 13 /// <summary> 14 /// 靜態初始化單例,只有通過這種寫法並且調用這個字段方能是餓漢式單例 15 /// </summary> 16 private static readonly ReadonlySingleton2 _instance = new ReadonlySingleton2(); 17 /// <summary> 18 /// 私有化構造函數,使得類不可通過new來創建實例 19 /// </summary> 20 private ReadonlySingleton2() 21 { 22 Console.WriteLine($"normalX={normalX}"); 23 Console.WriteLine($"staticX={staticX}"); 24 Console.WriteLine($"normalY={normalY}"); 25 Console.WriteLine($"staticY={staticY}"); 26 } 27 28 //寫法1:通過屬性獲取實例 29 public static ReadonlySingleton2 Instance 30 { 31 get { return _instance; } 32 } 33 34 //寫法2:通過方法獲取實例 35 public static ReadonlySingleton2 GetInstance() 36 { 37 return _instance; 38 } 39 40 #endregion 41 42 #region 其他 43 44 public int normalY = SetValue(staticY); 45 public static int staticY = 1; 46 47 private static int SetValue(int val) 48 { 49 return val; 50 } 51 52 #endregion 53 }
5.2調用過程

1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Console.WriteLine("程序開始"); 6 7 var tmp = Singleton.ReadonlySingleton2.Instance; 8 9 Console.ReadKey(); 10 } 11 }
5.3運行效果
5.4說明
如此寫便可避免類一加載就實例化對象。當然還可以有其他寫法。
normalX、staticX、normalY、staticY和Instance的執行順序為:外部調用》staticX 》Instance》normalX》normalY》構造函數》staticY。
可以看出,單例類字段都是在外部第一次調用之后才初始化執行的,這種寫法實現了延遲加載。
6.靜態初始化單例類示例3(靜態構造函數)
6.1代碼

1 public sealed class ReadonlySingleton3 2 { 3 #region 字段 4 5 public int normalX = SetValue(staticX); 6 7 public static int staticX = 1; 8 9 #endregion 10 11 #region 單例 12 /// <summary> 13 /// 靜態初始化單例,只有通過這種寫法並且調用這個字段方能是餓漢式單例 14 /// </summary> 15 public static readonly ReadonlySingleton3 Instance = new ReadonlySingleton3(); 16 17 /// <summary> 18 /// 定義一個靜態構造函數,使得靜態字段在調用的時候才被初始化 19 /// </summary> 20 static ReadonlySingleton3() 21 { 22 Console.WriteLine("ReadonlySingleton3____Static"); 23 } 24 /// <summary> 25 /// 私有化構造函數,使得類不可通過new來創建實例 26 /// </summary> 27 private ReadonlySingleton3() 28 { 29 Console.WriteLine($"normalX={normalX}"); 30 Console.WriteLine($"staticX={staticX}"); 31 Console.WriteLine($"normalY={normalY}"); 32 Console.WriteLine($"staticY={staticY}"); 33 } 34 35 #endregion 36 37 #region 其他 38 39 public int normalY = SetValue(staticY); 40 41 public static int staticY = 1; 42 43 public void ShowLog() 44 { 45 Console.WriteLine($"{Common.TimeHelper.PrintMillisecond()}:ReadonlySingleton3 ShowLog"); 46 } 47 48 private static int SetValue(int val) 49 { 50 return val; 51 } 52 53 #endregion 54 }
6.2調用過程

1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Console.WriteLine("程序開始"); 6 7 var tmp = Singleton.ReadonlySingleton3.Instance; 8 9 Console.ReadKey(); 10 } 11 }
6.3運行效果
6.4說明
這里定義了一個靜態構造函數,使得同為靜態類型的單例類實例Instance在第一次調用時才進行初始化,也就達到了不提前占用系統資源的目的。不過在初始化單例類時,會先調用私有構造函數,然后再調用靜態構造函數。
normalX、staticX、normalY、staticY和Instance的執行順序為:外部調用》staticX 》Instance》normalX》normalY》私有構造函數》staticY》靜態構造函數。
可以看出,這樣寫也能達到延遲加載的目的。
7.靜態初始化單例類示例4(內部類)
7.1代碼

1 public sealed class ReadonlySingleton4 2 { 3 #region 字段 4 5 public int normalX = SetValue(staticX); 6 7 public static int staticX = 1; 8 9 #endregion 10 11 #region 單例 12 private class InnerInstance 13 { 14 internal static ReadonlySingleton4 _instance = new ReadonlySingleton4(); 15 } 16 17 private ReadonlySingleton4() 18 { 19 Console.WriteLine($"normalX={normalX}"); 20 Console.WriteLine($"staticX={staticX}"); 21 Console.WriteLine($"normalY={normalY}"); 22 Console.WriteLine($"staticY={staticY}"); 23 } 24 25 public static ReadonlySingleton4 Instance 26 { 27 get { return InnerInstance._instance; } 28 } 29 30 public static ReadonlySingleton4 GetInstance() 31 { 32 return InnerInstance._instance; 33 } 34 #endregion 35 36 #region 其他 37 38 public int normalY = SetValue(staticY); 39 40 public static int staticY = 1; 41 42 public void ShowLog() 43 { 44 Console.WriteLine($"{Common.TimeHelper.PrintMillisecond()}:ReadonlySingleton4 ShowLog"); 45 } 46 47 private static int SetValue(int val) 48 { 49 return val; 50 } 51 52 #endregion 53 }
7.2調用過程

1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Console.WriteLine("程序開始"); 6 7 var tmp = Singleton.ReadonlySingleton4.Instance; 8 9 Console.ReadKey(); 10 } 11 }
7.3運行效果
7.4說明
normalX、staticX、normalY、staticY和Instance的執行順序為:外部調用》staticX 》staticY》內部類_instance》normalX》normalY》私有構造函數》內部類靜態構造函數(可以不用)。
內部類要不要構造函數和靜態構造函數都不影響,都能夠達到延遲加載的目的。
8.懶加載Lazy
8.1代碼

1 /// <summary> 2 /// 使用Lazy創建單例類示例 3 /// </summary> 4 public sealed class LazySingleton 5 { 6 #region 字段 7 8 public int normalX = SetValue(staticX); 9 10 public static int staticX = 1; 11 12 #endregion 13 14 #region 單例 15 private static readonly Lazy<LazySingleton> _instance = new Lazy<LazySingleton>(() => new LazySingleton()); 16 17 private LazySingleton() 18 { 19 Console.WriteLine($"normalX={normalX}"); 20 Console.WriteLine($"staticX={staticX}"); 21 Console.WriteLine($"normalY={normalY}"); 22 Console.WriteLine($"staticY={staticY}"); 23 } 24 25 //寫法1:通過屬性獲取實例 26 public static LazySingleton Instance 27 { 28 get 29 { 30 return _instance.Value; 31 } 32 } 33 34 //寫法2:通過方法獲取實例 35 public static LazySingleton GetInstance() 36 { 37 return _instance.Value; 38 } 39 #endregion 40 41 #region 其他 42 43 public int normalY = SetValue(staticY); 44 45 public static int staticY = 1; 46 47 private static int SetValue(int val) 48 { 49 return val; 50 } 51 52 #endregion 53 }
8.2調用過程

1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Console.WriteLine("程序開始"); 6 7 var tmp = Singleton.LazySingleton.Instance; 8 9 Console.ReadKey(); 10 } 11 }
8.3運行效果
8.4說明
normalX、staticX、normalY、staticY和Instance的執行順序為:外部調用》staticX 》單例對象_instance》staticY》normalX》normalY》私有構造函數。
9.總結
單例模式實現方式很多種,上面只是常用的幾種。我認為最適合使用的非"單例私有化"的寫法,既避免了多線程安全問題,又能延遲加載,避免提前占用系統資源。
學習了單例模式,接下來繼續工廠模式的學習。