單例模式簡介 單例模式是GOF 23個設計模式中最簡單的模式了,它提供了一種創建唯一對象的最佳實現,註意此處的簡單隻是表述和意圖很簡單,但是實現起來,尤其是實現一個優美的單例模式卻沒有那麼簡單。 單例模式歸根結底就是要確保一個類只有一個實例,並提供一個全局方式來訪問該實例。具體而言,這種模式涉及到一 ...
單例模式簡介
單例模式是GOF 23個設計模式中最簡單的模式了,它提供了一種創建唯一對象的最佳實現,註意此處的簡單隻是表述和意圖很簡單,但是實現起來,尤其是實現一個優美的單例模式卻沒有那麼簡單。
單例模式歸根結底就是要確保一個類只有一個實例,並提供一個全局方式來訪問該實例。具體而言,這種模式涉及到一個類,並由這個類創建自己的對象,同時確保只有單個對象被創建,並提供唯一一種方式來訪問該對象的實例。
在現實生活中,單例的場景有很多,比如一夫一妻制(當然不道德的除外),比如一個部門只有一個領導等等。
單例模式UML類圖
如上圖所示:
1、單例類只能有一個實例。
2、單例類必須自己創建自己的唯一實例。
3、單例類必須給所有其他對象提供這一實例。
4、構造函數是私有的。
範例
Double-Check
我們先看一個非常流行而又簡單的實現
1: public sealed class Singleton
2: {
3: private static Singleton instance = null;
4: private static readonly object padlock = new object();
5:
6: private Singleton()
7: {
8: }
9:
10: public static Singleton Instance
11: {
12: get
13: {
14: if (instance == null)
15: {
16: lock (padlock)
17: {
18: if (instance == null)
19: {
20: instance = new Singleton();
21: //Do a heavy task
22: }
23: }
24: }
25: return instance;
26: }
27: }
28: }
上述解決方案上,使用到了Double-Check方式,Double-Check方式可以說是盛名已久了,線程A與線程B在Null Check時同時通過,但是在Lock時,只能進入一個線程,其他線程都要等著。
這種方式在Java中編寫單例模式的時候是失效的,具體原因我沒有去深究。這一塊記憶體屏障技術(Memory Barrier),不過這段涉及到底層操作,一般很難有人會顯式操作,而且這段的控制異常複雜。另外一點就是,如果單例過程中操作的是一個數組或者其他對象,那麼在實例化後如果需要進行賦值等運算操作的,那麼其他線程在進行Null Check的時候就不會再次進入,如果其他線程調用了這個單例對象的某個屬性,這極有可能出現難以預測的bug。
單例模式載入數據到記憶體,那麼如果我們需要在使用的時候再去載入到記憶體,而不是一開始就載入到記憶體,這樣可以節省記憶體空間。接下來我們看一下如何通過懶載入方式實現單例模式。
靜態類
採用靜態類實現單例模式,這並不是一種完全的懶載入,但依然是線程安全的1: public sealed class Singleton
2: {
3: private static readonly Singleton instance = new Singleton();
4:
5: static Singleton()
6: {
7:
8: }
9:
10: private Singleton()
11: {
12:
13: }
14:
15: public static Singleton Instance
16: {
17: get
18: {
19: return instance;
20: }
21: }
22: }
C#中的靜態構造函數僅在創建類的實例或引用靜態成員時執行,並且每個AppDomain只執行一次,因為每次都需要對新構造的類型執行這種檢查,所以這種方式要比Double-Check方式更快。然而,也有一些問題:
- 它不像其他實現那樣懶惰。尤其是,如果您有實例以外的靜態成員,那麼對這些成員的第一個引用將涉及創建實例。這將在下一個實現中得到糾正。
- 如果一個靜態構造函數調用另一個靜態構造函數,而另一個靜態構造函數再次調用第一個靜態構造函數,則會出現複雜情況。需要註意,靜態構造函數在一個迴圈中相互引用的後果。
- 只有當類型沒有被[beforefieldinit]標記時,.NET才能保證類型初始值設定項的惰性。不幸的是,C編譯器(至少在.NET 1.1運行時中提供)將沒有靜態構造函數的所有類型(即看起來像構造函數但被標記為靜態的塊)標記為beforefieldinit。需要註意beforefieldinit會影響性能,beforefieldinit的具體用法可以參見MSDN。
對於這個實現,許多人更喜歡擁有一個屬性,以防將來需要進一步的操作,並且JIT內聯可能使性能相同。另外有一種快捷方式就是,可以將實例設置為公共的靜態只讀變數,不設置為屬性,這樣代碼的基本框架會顯得非常小。(註意,如果需要惰性,靜態構造函數本身仍然是必需的。)
內部類
採用內部類,這是一種完全的懶載入。
1: public sealed class Singleton
2: {
3: private Singleton()
4: {
5:
6: }
7:
8: public static Singleton Instance { get { return Nested.instance; } }
9:
10: private class Nested
11: {
12: static Nested()
13: {
14:
15: }
16:
17: internal static readonly Singleton instance = new Singleton();
18: }
19: }
在這裡,嵌套類的靜態成員在第一次引用的時候會進行實例化操作,並且該引用只在實例中發生。這意味著實現是完全懶惰的,但具有前一個實現的所有性能優勢。請註意,儘管嵌套類可以訪問內部類的私有成員,但反過來卻不是,因此需要在此處對實例進行內部訪問。不過,這並不會引發任何問題,因為類本身是私有的。不過此處貌似顯得有點複雜。
Lazy
那麼有沒有其他方式優雅而又安全的實現單例模式呢,答案是有的,那就是通過Lazy方式,Lazy方式可以擁有更高的性能,因為實例只有在使用的時候才會真正創建對象,這就在很大程度上減少了記憶體的占用,當然,比較如果是比較簡單的單例創建,可以忽略這條不利影響。
Lazy自帶Double-Check,是線程安全的,他就像一個盾牌,在創建過程中,不管是創建簡單對象還是複雜對象,都不會允許其他線程使用尚未創建完成的對象,更多的Lazy使用,請參考MSDN。
1: public sealed class Singleton
2: {
3: private Lazy<Singleton> lazy;
4:
5: public Singleton Instance { get { return lazy.Value; } }
6:
7: public string Name{get;set;}
8:
9: public Singleton()
10: {
11: lazy = new Lazy<Singleton>(InitializeSingleton);
12: }
13:
14: private Singleton InitializeSingleton()
15: {
16: Singleton singleton = new Singleton();
17: singleton.Name="Test";
18: return singleton;
19: }
20: }
單例模式優缺點
優點:
全局範圍內只有一個實例,避免了記憶體消耗,以及實例頻繁的創建和銷毀
避免了對資源的多重占用,比如獨占式場景中
缺點:
一旦對象指向的外部環境發生了變化,比如在網路調用、MQ等場景中一般可以可以採用單例,但是這裡需要提醒的是,如果DNS發生異常,在異常期間將會出現極難修複的情況,除非手動重啟並指向新的域伺服器
這一點有點違反單一職責原則,通常情況下,一個類應該只關註自身邏輯而不是創建對象
沒有介面,無法繼承
本文參考了https://csharpindepth.com/articles/Singleton,該文也是深入理解C#的作者所寫,可以收藏此網站以便更快的獲取相關信息。