c#單例模式(Singleton)的6種實(shí)現(xiàn)
1.1.1 摘要
在我們?nèi)粘5墓ぷ髦薪?jīng)常需要在應(yīng)用程序中保持一個(gè)唯一的實(shí)例,如:IO處理,數(shù)據(jù)庫(kù)操作等,由于這些對(duì)象都要占用重要的系統(tǒng)資源,所以我們必須限制這些實(shí)例的創(chuàng)建或始終使用一個(gè)公用的實(shí)例,這就是我們今天要介紹的——單例模式(Singleton)。
使用頻率高
單件模式(Singleton):保證一個(gè)類僅有一個(gè)實(shí)例,并提供一個(gè)訪問(wèn)它的全局訪問(wèn)點(diǎn)。
1.1.2 正文
圖1單例模式(Singleton)結(jié)構(gòu)圖
單例模式(Singleton)是幾個(gè)創(chuàng)建模式中最對(duì)立的一個(gè),它的主要特點(diǎn)不是根據(jù)用戶程序調(diào)用生成一個(gè)新的實(shí)例,而是控制某個(gè)類型的實(shí)例唯一性,通過(guò)上圖我們知道它包含的角色只有一個(gè),就是Singleton,它擁有一個(gè)私有構(gòu)造函數(shù),這確保用戶無(wú)法通過(guò)new直接實(shí)例它。除此之外,該模式中包含一個(gè)靜態(tài)私有成員變量instance與靜態(tài)公有方法Instance()。Instance()方法負(fù)責(zé)檢驗(yàn)并實(shí)例化自己,然后存儲(chǔ)在靜態(tài)成員變量中,以確保只有一個(gè)實(shí)例被創(chuàng)建。
圖2單例模式(Singleton)邏輯模型
接下來(lái)我們將介紹6中不同的單例模式(Singleton)的實(shí)現(xiàn)方式。這些實(shí)現(xiàn)方式都有以下的共同點(diǎn):
1.有一個(gè)私有的無(wú)參構(gòu)造函數(shù),這可以防止其他類實(shí)例化它,而且單例類也不應(yīng)該被繼承,如果單例類允許繼承那么每個(gè)子類都可以創(chuàng)建實(shí)例,這就違背了Singleton模式“唯一實(shí)例”的初衷。
2.單例類被定義為sealed,就像前面提到的該類不應(yīng)該被繼承,所以為了保險(xiǎn)起見(jiàn)可以把該類定義成不允許派生,但沒(méi)有要求一定要這樣定義。
3.一個(gè)靜態(tài)的變量用來(lái)保存單實(shí)例的引用。
4.一個(gè)公有的靜態(tài)方法用來(lái)獲取單實(shí)例的引用,如果實(shí)例為null即創(chuàng)建一個(gè)。
版本一線程不安全
/// <summary> /// A simple singleton class implements. /// </summary> public sealed class Singleton { private static Singleton _instance = null; /// <summary> /// Prevents a default instance of the /// <see cref="Singleton"/> class from being created. /// </summary> private Singleton() { } /// <summary> /// Gets the instance. /// </summary> public static Singleton Instance { get { return _instance ?? (_instance = new Singleton()); } } }
以上的實(shí)現(xiàn)方式適用于單線程環(huán)境,因?yàn)樵诙嗑€程的環(huán)境下有可能得到Singleton類的多個(gè)實(shí)例。假如同時(shí)有兩個(gè)線程去判斷
(null == _singleton),并且得到的結(jié)果為真,那么兩個(gè)線程都會(huì)創(chuàng)建類Singleton的實(shí)例,這樣就違背了Singleton模式“唯一實(shí)例”的初衷。
版本二線程安全
/// <summary> /// A thread-safe singleton class. /// </summary> public sealed class Singleton { private static Singleton _instance = null; private static readonly object SynObject = new object(); Singleton() { } /// <summary> /// Gets the instance. /// </summary> public static Singleton Instance { get { // Syn operation. lock (SynObject) { return _instance ?? (_instance = new Singleton()); } } } }
以上方式的實(shí)現(xiàn)方式是線程安全的,首先我們創(chuàng)建了一個(gè)靜態(tài)只讀的進(jìn)程輔助對(duì)象,由于lock是確保當(dāng)一個(gè)線程位于代碼的臨界區(qū)時(shí),另一個(gè)線程不能進(jìn)入臨界區(qū)(同步操作)。如果其他線程試圖進(jìn)入鎖定的代碼,則它將一直等待,直到該對(duì)象被釋放。從而確保在多線程下不會(huì)創(chuàng)建多個(gè)對(duì)象實(shí)例了。只是這種實(shí)現(xiàn)方式要進(jìn)行同步操作,這將是影響系統(tǒng)性能的瓶頸和增加了額外的開(kāi)銷。
Double-Checked Locking
前面講到的線程安全的實(shí)現(xiàn)方式的問(wèn)題是要進(jìn)行同步操作,那么我們是否可以降低通過(guò)操作的次數(shù)呢?其實(shí)我們只需在同步操作之前,添加判斷該實(shí)例是否為null就可以降低通過(guò)操作的次數(shù)了,這樣是經(jīng)典的Double-Checked Locking方法。
/// <summary> /// Double-Checked Locking implements a thread-safe singleton class /// </summary> public sealed class Singleton { private static Singleton _instance = null; // Creates an syn object. private static readonly object SynObject = new object(); Singleton() { } public static Singleton Instance { get { // Double-Checked Locking if (null == _instance) { lock (SynObject) { if (null == _instance) { _instance = new Singleton(); } } } return _instance; } } }
在介紹第四種實(shí)現(xiàn)方式之前,首先讓我們認(rèn)識(shí)什么是,當(dāng)字段被標(biāo)記為beforefieldinit類型時(shí),該字段初始化可以發(fā)生在任何時(shí)候任何字段被引用之前。這句話聽(tīng)起了有點(diǎn)別扭,接下來(lái)讓我們通過(guò)具體的例子介紹。
/// <summary> /// Defines a test class. /// </summary> class Test { public static string x = EchoAndReturn("In type initializer"); public static string EchoAndReturn(string s) { Console.WriteLine(s); return s; } }
上面我們定義了一個(gè)包含靜態(tài)字段和方法的類Test,但要注意我們并沒(méi)有定義靜態(tài)的構(gòu)造函數(shù)。
圖3 Test類的IL代碼
class Test { public static string x = EchoAndReturn("In type initializer"); // Defines a parameterless constructor. static Test() { } public static string EchoAndReturn(string s) { Console.WriteLine(s); return s; } }
上面我們給Test類添加一個(gè)靜態(tài)的構(gòu)造函數(shù)。
圖4 Test類的IL代碼
通過(guò)上面Test類的IL代碼的區(qū)別我們發(fā)現(xiàn),當(dāng)Test類包含靜態(tài)字段,而且沒(méi)有定義靜態(tài)的構(gòu)造函數(shù)時(shí),該類會(huì)被標(biāo)記為beforefieldinit。
現(xiàn)在也許有人會(huì)問(wèn):“被標(biāo)記為beforefieldinit和沒(méi)有標(biāo)記的有什么區(qū)別呢”?OK現(xiàn)在讓我們通過(guò)下面的具體例子看一下它們的區(qū)別吧!
class Test { public static string x = EchoAndReturn("In type initializer"); static Test() { } public static string EchoAndReturn(string s) { Console.WriteLine(s); return s; } } class Driver { public static void Main() { Console.WriteLine("Starting Main"); // Invoke a static method on Test Test.EchoAndReturn("Echo!"); Console.WriteLine("After echo"); Console.ReadLine(); // The output result: // Starting Main // In type initializer // Echo! // After echo } }
我相信大家都可以得到答案,如果在調(diào)用EchoAndReturn()方法之前,需要完成靜態(tài)成員的初始化,所以最終的輸出結(jié)果如下:
圖5輸出結(jié)果
接著我們?cè)贛ain()方法中添加string y = Test.x,如下:
public static void Main() { Console.WriteLine("Starting Main"); // Invoke a static method on Test Test.EchoAndReturn("Echo!"); Console.WriteLine("After echo"); //Reference a static field in Test string y = Test.x; //Use the value just to avoid compiler cleverness if (y != null) { Console.WriteLine("After field access"); } Console.ReadKey(); // The output result: // In type initializer // Starting Main // Echo! // After echo // After field access }
圖6 輸出結(jié)果
通過(guò)上面的輸出結(jié)果,大家可以發(fā)現(xiàn)靜態(tài)字段的初始化跑到了靜態(tài)方法調(diào)用之前,Wo難以想象??!
最后我們?cè)赥est類中添加一個(gè)靜態(tài)構(gòu)造函數(shù)如下:
class Test { public static string x = EchoAndReturn("In type initializer"); static Test() { } public static string EchoAndReturn(string s) { Console.WriteLine(s); return s; } }
圖7 輸出結(jié)果
理論上,type initializer應(yīng)該發(fā)生在”Echo!”之后和”After echo”之前,但這里卻出現(xiàn)了不唯一的結(jié)果,只有當(dāng)Test類包含靜態(tài)構(gòu)造函數(shù)時(shí),才能確保type initializer的初始化發(fā)生在”Echo!”之后和”After echo”之前。
所以說(shuō)要確保type initializer發(fā)生在被字段引用時(shí),我們應(yīng)該給該類添加靜態(tài)構(gòu)造函數(shù)。接下來(lái)讓我們介紹單例模式的靜態(tài)方式。
靜態(tài)初始化
public sealed class Singleton { private static readonly Singleton _instance = new Singleton(); // Explicit static constructor to tell C# compiler // not to mark type as beforefieldinit static Singleton() { } /// <summary> /// Prevents a default instance of the /// <see cref="Singleton"/> class from being created. /// </summary> private Singleton() { } /// <summary> /// Gets the instance. /// </summary> public static Singleton Instance { get { return _instance; } } }
以上方式實(shí)現(xiàn)比之前介紹的方式都要簡(jiǎn)單,但它確實(shí)是多線程環(huán)境下,C#實(shí)現(xiàn)的Singleton的一種方式。由于這種靜態(tài)初始化的方式是在自己的字段被引用時(shí)才會(huì)實(shí)例化。
讓我們通過(guò)IL代碼來(lái)分析靜態(tài)初始化。
圖8靜態(tài)初始化IL代碼
首先這里沒(méi)有beforefieldinit的修飾符,由于我們添加了靜態(tài)構(gòu)造函數(shù)當(dāng)靜態(tài)字段被引用時(shí)才進(jìn)行初始化,因此即便很多線程試圖引用_instance,也需要等靜態(tài)構(gòu)造函數(shù)執(zhí)行完并把靜態(tài)成員_instance實(shí)例化之后可以使用。
延遲初始化
/// <summary> /// Delaies initialization. /// </summary> public sealed class Singleton { private Singleton() { } /// <summary> /// Gets the instance. /// </summary> public static Singleton Instance { get { return Nested._instance; } } private class Nested { // Explicit static constructor to tell C# compiler // not to mark type as beforefieldinit static Nested() { } internal static readonly Singleton _instance = new Singleton(); } }
這里我們把初始化工作放到Nested類中的一個(gè)靜態(tài)成員來(lái)完成,這樣就實(shí)現(xiàn)了延遲初始化。
Lazy<T> type
/// <summary> /// .NET 4's Lazy<T> type /// </summary> public sealed class Singleton { private static readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton()); public static Singleton Instance { get { return lazy.Value; } } private Singleton() { } }
這種方式的簡(jiǎn)單和性能良好,而且還提供檢查是否已經(jīng)創(chuàng)建實(shí)例的屬性IsValueCreated。
具體例子
現(xiàn)在讓我們使用單例模式(Singleton)實(shí)現(xiàn)負(fù)載平衡器,首先我們定義一個(gè)服務(wù)器類,它包含服務(wù)器名和IP地址如下:
/// <summary> /// Represents a server machine /// </summary> class Server { // Gets or sets server name public string Name { get; set; } // Gets or sets server IP address public string IP { get; set; } }
由于負(fù)載平衡器只提供一個(gè)對(duì)象實(shí)例供服務(wù)器使用,所以我們使用單例模式(Singleton)實(shí)現(xiàn)該負(fù)載平衡器。
/// <summary> /// The 'Singleton' class /// </summary> sealed class LoadBalancer { private static readonly LoadBalancer _instance = new LoadBalancer(); // Type-safe generic list of servers private List<Server> _servers; private Random _random = new Random(); static LoadBalancer() { } // Note: constructor is 'private' private LoadBalancer() { // Load list of available servers _servers = new List<Server> { new Server{ Name = "ServerI", IP = "192.168.0.108" }, new Server{ Name = "ServerII", IP = "192.168.0.109" }, new Server{ Name = "ServerIII", IP = "192.168.0.110" }, new Server{ Name = "ServerIV", IP = "192.168.0.111" }, new Server{ Name = "ServerV", IP = "192.168.0.112" }, }; } /// <summary> /// Gets the instance through static initialization. /// </summary> public static LoadBalancer Instance { get { return _instance; } } // Simple, but effective load balancer public Server NextServer { get { int r = _random.Next(_servers.Count); return _servers[r]; } } }
上面負(fù)載平衡器類LoadBalancer我們使用靜態(tài)初始化方式實(shí)現(xiàn)單例模式(Singleton)。
static void Main() { LoadBalancer b1 = LoadBalancer.Instance; b1.GetHashCode(); LoadBalancer b2 = LoadBalancer.Instance; LoadBalancer b3 = LoadBalancer.Instance; LoadBalancer b4 = LoadBalancer.Instance; // Confirm these are the same instance if (b1 == b2 && b2 == b3 && b3 == b4) { Console.WriteLine("Same instance\n"); } // Next, load balance 15 requests for a server LoadBalancer balancer = LoadBalancer.Instance; for (int i = 0; i < 15; i++) { string serverName = balancer.NextServer.Name; Console.WriteLine("Dispatch request to: " + serverName); } Console.ReadKey(); }
圖9 LoadBalancer輸出結(jié)果
1.1.3 總結(jié)
單例模式的優(yōu)點(diǎn):
單例模式(Singleton)會(huì)控制其實(shí)例對(duì)象的數(shù)量,從而確保訪問(wèn)對(duì)象的唯一性。
1.實(shí)例控制:?jiǎn)卫J椒乐蛊渌鼘?duì)象對(duì)自己的實(shí)例化,確保所有的對(duì)象都訪問(wèn)一個(gè)實(shí)例。
2.伸縮性:因?yàn)橛深愖约簛?lái)控制實(shí)例化進(jìn)程,類就在改變實(shí)例化進(jìn)程上有相應(yīng)的伸縮性。
單例模式的缺點(diǎn):
1.系統(tǒng)開(kāi)銷。雖然這個(gè)系統(tǒng)開(kāi)銷看起來(lái)很小,但是每次引用這個(gè)類實(shí)例的時(shí)候都要進(jìn)行實(shí)例是否存在的檢查。這個(gè)問(wèn)題可以通過(guò)靜態(tài)實(shí)例來(lái)解決。
2.開(kāi)發(fā)混淆。當(dāng)使用一個(gè)單例模式的對(duì)象的時(shí)候(特別是定義在類庫(kù)中的),開(kāi)發(fā)人員必須要記住不能使用new關(guān)鍵字來(lái)實(shí)例化對(duì)象。因?yàn)殚_(kāi)發(fā)者看不到在類庫(kù)中的源代碼,所以當(dāng)他們發(fā)現(xiàn)不能實(shí)例化一個(gè)類的時(shí)候會(huì)很驚訝。
3.對(duì)象生命周期。單例模式?jīng)]有提出對(duì)象的銷毀。在提供內(nèi)存管理的開(kāi)發(fā)語(yǔ)言(比如,基于.NetFramework的語(yǔ)言)中,只有單例模式對(duì)象自己才能將對(duì)象實(shí)例銷毀,因?yàn)橹挥兴鼡碛袑?duì)實(shí)例的引用。在各種開(kāi)發(fā)語(yǔ)言中,比如C++,其它類可以銷毀對(duì)象實(shí)例,但是這么做將導(dǎo)致單例類內(nèi)部的指針指向不明。
單例適用性
使用Singleton模式有一個(gè)必要條件:在一個(gè)系統(tǒng)要求一個(gè)類只有一個(gè)實(shí)例時(shí)才應(yīng)當(dāng)使用單例模式。反之,如果一個(gè)類可以有幾個(gè)實(shí)例共存,就不要使用單例模式。
不要使用單例模式存取全局變量。這違背了單例模式的用意,最好放到對(duì)應(yīng)類的靜態(tài)成員中。
不要將數(shù)據(jù)庫(kù)連接做成單例,因?yàn)橐粋€(gè)系統(tǒng)可能會(huì)與數(shù)據(jù)庫(kù)有多個(gè)連接,并且在有連接池的情況下,應(yīng)當(dāng)盡可能及時(shí)釋放連接。Singleton模式由于使用靜態(tài)成員存儲(chǔ)類實(shí)例,所以可能會(huì)造成資源無(wú)法及時(shí)釋放,帶來(lái)問(wèn)題。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
C# Entity Framework中的IQueryable和IQueryProvider詳解
這篇文章主要介紹了C# Entity Framework中的IQueryable和IQueryProvider詳解,本文使用實(shí)例分析這兩個(gè)接口的內(nèi)部實(shí)現(xiàn),需要的朋友可以參考下2015-01-01C#實(shí)現(xiàn)帶百分比的進(jìn)度條功能示例
這篇文章主要介紹了C#實(shí)現(xiàn)帶百分比的進(jìn)度條功能,分析了帶百分比進(jìn)度條的功能需求并結(jié)合實(shí)例形式給出了具體實(shí)現(xiàn)步驟與相關(guān)操作方法,需要的朋友可以參考下2017-05-05C# 創(chuàng)建、部署和調(diào)用WebService簡(jiǎn)單示例
這篇文章主要為大家詳細(xì)介紹了C# 創(chuàng)建、部署和調(diào)用WebService的簡(jiǎn)單示例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05詳解WPF如何在Panel中實(shí)現(xiàn)設(shè)置所有子項(xiàng)間距
這篇文章主要為大家詳細(xì)介紹了WPF如何在Panel中實(shí)現(xiàn)設(shè)置所有子項(xiàng)間距,本文借鑒了 Qt 中的 Spacing 設(shè)置方法,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-10-10