一、引言 單例模式應該算是23種設計模式中比較簡單的,它屬於創建型的設計模式,關註對象的創建。 二、概念 單例模式是23個“Gang Of Four”的設計模式之一,它描述瞭如何解決重覆出現的設計問題,以設計靈活且可復用的面向對象軟體,使對象的實現、更改、測試和重用更方便。 單例模式解決了以下問題: ...
一、引言
單例模式應該算是23種設計模式中比較簡單的,它屬於創建型的設計模式,關註對象的創建。
二、概念
單例模式是23個“Gang Of Four”的設計模式之一,它描述瞭如何解決重覆出現的設計問題,以設計靈活且可復用的面向對象軟體,使對象的實現、更改、測試和重用更方便。
單例模式解決了以下問題:
-
如何確保類只有一個實例?
- 如何輕鬆地訪問類的唯一實例?
-
如何控制類的實例化?
-
如何限制類的實例數量?
單例模式是如何解決以上問題的呢?
- 隱藏類的構造函數。
- 定義一個返回類的唯一實例的公共靜態操作。
這個設計模式的關鍵點在於使類控制其自身的實例化。
隱藏類的構造函數(定義私有構造函數)來確保類不能從外部實例化。
使用靜態函數輕鬆訪問類實例(Singleton.getInstance())。
三、實現
1、懶漢式單例
1 using System.Threading;
2
3 public class SingletonTest
4 {
5 private static SingletonTest instance = null;
6
7 /// <summary>
8 /// 隱藏類的構造函數(定義私有構造函數)來確保類不能從外部實例化
9 /// </summary>
10 private SingletonTest()
11 {
12 Console.WriteLine("******單例類被實例化******");
13 }
14
15 /// <summary>
16 /// 使用靜態函數輕鬆訪問類實例
17 /// </summary>
18 /// <returns><see cref="SingletonTest"/></returns>
19 public static SingletonTest GetInstance()
20 {
21 if (instance == null)
22 {
23 instance = new SingletonTest();
24 }
25
26 return instance;
27 }
28
29 public void PrintSomething()
30 {
31 Console.WriteLine($"當前線程Id為{Thread.CurrentThread.ManagedThreadId}");
32 Console.WriteLine("Singleton Pattern Test");
33 }
34 }
上述代碼在單線程的情況下是能正常運行的,符合單例模式的定義。
但是,多線程的情況呢?
我們使用以下代碼進行測試
1 // 多線程情況
2 for (int i = 0; i < 10; i++)
3 {
4 Task.Run(
5 () =>
6 {
7 SingletonTest singleton1 = SingletonTest.GetInstance();
8 singleton1.PrintSomething();
9 });
10 }
結果如下
不出所料,類SingletonTest被實例化了多次,不符合單例模式的要求,那如何解決這個問題呢?
既然是多線程引起的問題,那就要使用線程同步。我們在這裡使用鎖來實現。
1 using System;
2 using System.Threading;
3
4 public class SingletonTest
5 {
6 private static SingletonTest instance = null;
7
8 private static object lockObject = new object();
9
10 /// <summary>
11 /// 隱藏類的構造函數(定義私有構造函數)來確保類不能從外部實例化
12 /// </summary>
13 private SingletonTest()
14 {
15 Thread.Sleep(500);
16 Console.WriteLine("******單例類被實例化******");
17 }
18
19 /// <summary>
20 /// 使用靜態函數輕鬆訪問類實例
21 /// </summary>
22 /// <returns><see cref="SingletonTest"/></returns>
23 public static SingletonTest GetInstance()
24 {
25 // 雙重檢查鎖定的方式實現單例模式
26 if (instance == null)
27 {
28 lock (lockObject)
29 {
30 if (instance == null)
31 {
32 instance = new SingletonTest();
33 }
34 }
35 }
36
37 return instance;
38 }
39
40 public void PrintSomething()
41 {
42 Console.WriteLine($"當前線程Id為{Thread.CurrentThread.ManagedThreadId}");
43 Console.WriteLine("Singleton Pattern Test");
44 }
45 }
上述代碼在創建實例前,檢查了兩次實例是否為空,第一次判空是否有必要呢(上述代碼26行)?為什麼呢?我們想象這樣的一種場景,在已經初始化類實例的情況下,是否還需要獲取鎖?答案是不需要,所以加第一個判斷條件(上述代碼26行)。
再次運行測試代碼,結果如下:
從結果來看,類只被實例化了一次,解決了多線程下類被多次實例化的問題。但是指令重排序是否對此有影響?是否需要volatile關鍵字?歡迎大佬來解答一下。
2、餓漢式單例(推薦)
第一種方式有點繁瑣,可以簡單點嗎?如下
1 using System;
2 using System.Threading;
3
4 public class SingletonTest2
5 {
6 // 靜態變數的方式實現單例模式
7 private static readonly SingletonTest2 Instance = new SingletonTest2();
8
9 private SingletonTest2()
10 {
11 Thread.Sleep(1000);
12 Console.WriteLine("******單例類被實例化******");
13 }
14
15 public static SingletonTest2 GetInstance()
16 {
17 return Instance;
18 }
19
20 public void PrintSomething()
21 {
22 Console.WriteLine($"當前線程Id為{Thread.CurrentThread.ManagedThreadId}");
23 Console.WriteLine("Singleton Pattern Test");
24 }
25 }
這種實現方式利用的是.NET中靜態關鍵字static的特性,使單例類在使用前被實例化,並且只實例化一次,這個由.NET框架保證。
這種方式存在問題,在沒有使用到類中的成員時候就創建實例了,能否在使用到類成員的時候才創建實例呢?如下圖
1 using System;
2 using System.Threading;
3
4 public class SingletonTest2
5 {
6 // 靜態變數的方式實現單例模式
7 private static readonly Lazy<SingletonTest2> Instance = new Lazy<SingletonTest2>(() => new SingletonTest2());
8
9 private SingletonTest2()
10 {
11 Thread.Sleep(1000);
12 Console.WriteLine("******初始化單例模式實例*****");
13 }
14
15 public static SingletonTest2 GetInstance()
16 {
17 return Instance.Value;
18 }
19
20 public void PrintSomething()
21 {
22 Console.WriteLine($"當前線程Id為{Thread.CurrentThread.ManagedThreadId}");
23 Console.WriteLine("Singleton Pattern Test");
24 }
25 }
我們使用了Lazy關鍵字來延遲實例化。
四、例外
值得註意的是,反射會破壞單例模式,如下代碼,能直接調用類的私有構造函數,再次實例化。
1 // 反射破壞單例
2 var singletonInstance = System.Activator.CreateInstance(typeof(SingletonTest2), true);
怎麼避免呢?類的實例化都需要調用構造函數,那麼我們在構造函數中加入判斷標識即可。嘗試實例化第二次的時候,就會拋異常。
1 private static bool isInstantiated;
2
3 private SingletonTest2()
4 {
5 if (isInstantiated)
6 {
7 throw new Exception("已經被實例化了,不能再次實例化");
8 }
9
10 isInstantiated = true;
11 Thread.Sleep(1000);
12 Console.WriteLine("******單例類被實例化******");
13 }
五、應用
那麼實際應用中,哪些地方應該用單例模式呢?在這個類只應該存在一個對象的情況下使用。哪些地方用到了單例模式呢?
- Windows任務管理器
- HttpContext.Current
六、總結
俗話說,凡事都有兩面性。單例模式確保了類只有一個實例,也引入了其他問題:
- 單例模式中的唯一實例變數是使用static標記的,會常駐記憶體,不被GC回收,長期占用了記憶體
- 在多線程的情況下,使用的都是同一個實例,所以需要保證類中的成員都是線程安全,不然可能會導致數據混亂的情況
代碼下載:https://github.com/hzhhhbb/SingletonPattern
七、參考資料
- https://en.wikipedia.org/wiki/Singleton_pattern
- https://docs.microsoft.com/zh-cn/dotnet/api/system.lazy-1?view=netcore-3.0