C#單例模式(Singleton Pattern)
一、單例模式(Singleton Pattern)
概述:
Singleton模式要求一個類有且僅有一個例項,並且提供了一個全域性的訪問點。這就提出了一個問題:如何繞過常規的構造器,提供一種機制來保證一個類只有一個例項?客戶程式在呼叫某一個類時,它是不會考慮這個類是否只能有一個例項等問題的,所以,這應該是類設計者的責任,而不是類使用者的責任。從另一個角度來說,Singleton模式其實也是一種職責型模式。因為我們建立了一個物件,這個物件扮演了獨一無二的角色,在這個單獨的物件例項中,它集中了它所屬類的所有權力,同時它也肩負了行使這種權力的職責!
特點:
- 單例類只能有一個例項。
- 單例類必須自己建立自己的唯一例項。
- 單例類必須給所有其它物件提供這一例項。
應用:
- 每臺計算機可以有若干個印表機,但只能有一個Printer Spooler,避免兩個列印作業同時輸出到印表機。
- 一個具有自動編號主鍵的表可以有多個使用者同時使用,但資料庫中只能有一個地方分配下一個主鍵編號。否則會出現主鍵重複。
意圖
保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點。
模型圖
邏輯模型圖:
物理模型圖:
二、Singleton模式的結構和五種實現方式
Singleton模式包含的角色只有一個,就是Singleton。Singleton擁有一個私有建構函式,確保使用者無法通過new直接例項它。除此之外,該模式中包含一個靜態私有成員變數instance與靜態公有方法Instance()。Instance方法負責檢驗並例項化自己,然後儲存在靜態成員變數中,以確保只有一個例項被建立。
1、簡單實現
該程式演示了Singleton的結構,本身不具有任何實際價值
public sealed class Singleton { static Singleton instance=null; Singleton() { } public static Singleton Instance { get { if (instance==null) { instance = new Singleton(); }return instance; } }
}
客戶端使用:
/// <summary> /// Client test /// </summary> public class Client { public static void Main() { Singleton s1 = Singleton.Instance(); Singleton s2 = Singleton.Instance(); if( s1 == s2 ) Console.WriteLine( "The same instance" ); } }
缺點:
- 這種方式的實現對於執行緒來說並不是安全的,因為在多執行緒的環境下有可能得到Singleton類的多個例項。如果同時有兩個執行緒去判斷(instance == null),並且得到的結果為真,這時兩個執行緒都會建立類Singleton的例項,這樣就違背了Singleton模式的原則。實際上在上述程式碼中,有可能在計算出表示式的值之前,物件例項已經被建立,但是記憶體模型並不能保證物件例項在第二個執行緒建立之前被發現。
優點:
- 由於例項是在Instance屬性方法內部建立的,因此類可以使用附加功能(例如,對子類進行例項化),即使它可能引入不想要的依賴性。
- 直到物件要求產生一個例項才執行例項化;這種方法稱為“惰性例項化”。惰性例項化避免了在應用程式啟動時例項化不必要的singleton。
2、安全的執行緒
public sealed class Singleton { static Singleton instance=null; static readonly object padlock = new object(); Singleton() { } public static Singleton Instance { get { lock (padlock) { if (instance==null) { instance = new Singleton(); } return instance; } } } }
這種方式的實現對於執行緒來說是安全的。我們首先建立了一個程序輔助物件,執行緒在進入時先對輔助物件加鎖然後再檢測物件是否被建立,這樣可以確保只有一個例項被建立,因為在同一個時刻加了鎖的那部分程式只有一個執行緒可以進入。這種情況下,物件例項由最先進入的那個執行緒建立,後來的執行緒在進入時(instence == null)為假,不會再去建立物件例項了。但是這種實現方式增加了額外的開銷,損失了效能。
3、雙重鎖定
public sealed class Singleton { static Singleton instance=null; static readonly object padlock = new object(); Singleton() { } public static Singleton Instance { get { if (instance==null) { lock (padlock) { if (instance==null) { instance = new Singleton(); } } } return instance; } } }
這種實現方式對多執行緒來說是安全的,同時執行緒不是每次都加鎖,只有判斷物件例項沒有被建立時它才加鎖,有了我們上面第一部分的裡面的分析,我們知道,加鎖後還得再進行物件是否已被建立的判斷。它解決了執行緒併發問題,同時避免在每個Instance屬性方法的呼叫中都出現獨佔鎖定。它還允許您將例項化延遲到第一次訪問物件時發生。實際上,應用程式很少需要這種型別的實現。大多數情況下我們會用靜態初始化。這種方式仍然有很多缺點:無法實現延遲初始化。
4、靜態初始化
public sealed class Singleton { static readonly Singleton instance=new Singleton(); static Singleton() { } Singleton() { } public static Singleton Instance { get { return instance; } } }
看到上面這段富有戲劇性的程式碼,我們可能會產生懷疑,這還是Singleton模式嗎?在此實現中,將在第一次引用類的任何成員時建立例項。公共語言執行庫負責處理變數初始化。該類標記為sealed以阻止發生派生,而派生可能會增加例項。此外,變數標記為readonly,這意味著只能在靜態初始化期間或在類建構函式中分配變數。該實現與前面的示例類似,不同之處在於它依賴公共語言執行庫來初始化變數。它仍然可以用來解決Singleton模式試圖解決的兩個基本問題:全域性訪問和例項化控制。公共靜態屬性為訪問例項提供了一個全域性訪問點。此外,由於建構函式是私有的,因此不能在類本身以外例項化Singleton類;因此,變數引用的是可以在系統中存在的唯一的例項。
由於Singleton例項被私有靜態成員變數引用,因此在類首次被對Instance屬性的呼叫所引用之前,不會發生例項化。這種方法唯一的潛在缺點是,您對例項化機制的控制權較少。在Design Patterns形式中,您能夠在例項化之前使用非預設的建構函式或執行其他任務。由於在此解決方案中由.NET Framework負責執行初始化,因此您沒有這些選項。在大多數情況下,靜態初始化是在.NET中實現Singleton的首選方法。
5、延遲初始化
public sealed class Singleton { Singleton() { } public static Singleton Instance { get { return Nested.instance; } } class Nested { static Nested() { } internal static readonly Singleton instance = new Singleton(); } }
這裡,初始化工作有Nested類的一個靜態成員來完成,這樣就實現了延遲初始化,並具有很多的優勢,是值得推薦的一種實現方式
三、總結
1、實現要點
- Singleton模式是限制而不是改進類的建立。
- Singleton類中的例項構造器可以設定為Protected以允許子類派生。
- Singleton模式一般不要支援Icloneable介面,因為這可能導致多個物件例項,與Singleton模式的初衷違背。
- Singleton模式一般不要支援序列化,這也有可能導致多個物件例項,這也與Singleton模式的初衷違背。
- Singleton只考慮了物件建立的管理,沒有考慮到銷燬的管理,就支援垃圾回收的平臺和物件的開銷來講,我們一般沒必要對其銷燬進行特殊的管理。
- 理解和擴充套件Singleton模式的核心是“如何控制使用者使用new對一個類的構造器的任意呼叫”。
- 可以很簡單的修改一個Singleton,使它有少數幾個例項,這樣做是允許的而且是有意義的。
2、優點
- 例項控制:Singleton會阻止其他物件例項化其自己的Singleton物件的副本,從而確保所有物件都訪問唯一例項
- 靈活性:因為類控制了例項化過程,所以類可以更加靈活修改例項化過程
3、缺點
- 開銷:雖然數量很少,但如果每次物件請求引用時都要檢查是否存在類的例項,將仍然需要一些開銷。可以通過使用靜態初始化解決此問題,上面的五種實現方式中已經說過了。
- 可能的開發混淆:使用singleton物件(尤其在類庫中定義的物件)時,開發人員必須記住自己不能使用new關鍵字例項化物件。因為可能無法訪問庫原始碼,因此應用程式開發人員可能會意外發現自己無法直接例項化此類。
- 物件的生存期:Singleton不能解決刪除單個物件的問題。在提供記憶體管理的語言中(例如基於.NET Framework的語言),只有Singleton類能夠導致例項被取消分配,因為它包含對該例項的私有引用。在某些語言中(如C++),其他類可以刪除
- 物件例項,但這樣會導致Singleton類中出現懸浮引用。
4、適用性
- 當類只能有一個例項而且客戶可以從一個眾所周知的訪問點訪問它時。
- 當這個唯一例項應該是通過子類化可擴充套件的,並且客戶應該無需更改程式碼就能使用一個擴充套件的例項時。
5、完整示例
下面這段Singleton程式碼演示了負載均衡物件。在負載均衡模型中,有多臺伺服器可提供服務,任務分配器隨機挑選一臺伺服器提供服務,以確保任務均衡(實際情況比這個複雜的多)。這裡,任務分配例項只能有一個,負責挑選伺服器並分配任務。
// Singleton pattern -- Real World example using System; using System.Collections; using System.Threading; // "Singleton" class LoadBalancer { // Fields private static LoadBalancer balancer; private ArrayList servers = new ArrayList(); private Random random = new Random(); // Constructors (protected) protected LoadBalancer() { // List of available servers servers.Add( "ServerI" ); servers.Add( "ServerII" ); servers.Add( "ServerIII" ); servers.Add( "ServerIV" ); servers.Add( "ServerV" ); } // Methods public static LoadBalancer GetLoadBalancer() { // Support multithreaded applications through // "Double checked locking" pattern which avoids // locking every time the method is invoked if( balancer == null ) { // Only one thread can obtain a mutex Mutex mutex = new Mutex(); mutex.WaitOne(); if( balancer == null ) balancer = new LoadBalancer(); mutex.Close(); } return balancer; } // Properties public string Server { get { // Simple, but effective random load balancer int r = random.Next( servers.Count ); return servers[ r ].ToString(); } } } /// <summary> /// SingletonApp test /// </summary> /// public class SingletonApp { public static void Main( string[] args ) { LoadBalancer b1 = LoadBalancer.GetLoadBalancer(); LoadBalancer b2 = LoadBalancer.GetLoadBalancer(); LoadBalancer b3 = LoadBalancer.GetLoadBalancer(); LoadBalancer b4 = LoadBalancer.GetLoadBalancer(); // Same instance? if( (b1 == b2) && (b2 == b3) && (b3 == b4) ) Console.WriteLine( "Same instance" ); // Do the load balancing Console.WriteLine( b1.Server ); Console.WriteLine( b2.Server ); Console.WriteLine( b3.Server ); Console.WriteLine( b4.Server ); } }