c#單例模式(Singleton)的6種實現(xiàn)
1.1.1 摘要
在我們?nèi)粘5墓ぷ髦薪?jīng)常需要在應用程序中保持一個唯一的實例,如:IO處理,數(shù)據(jù)庫操作等,由于這些對象都要占用重要的系統(tǒng)資源,所以我們必須限制這些實例的創(chuàng)建或始終使用一個公用的實例,這就是我們今天要介紹的——單例模式(Singleton)。
使用頻率
高
單件模式(Singleton):保證一個類僅有一個實例,并提供一個訪問它的全局訪問點。
1.1.2 正文

圖1單例模式(Singleton)結(jié)構(gòu)圖
單例模式(Singleton)是幾個創(chuàng)建模式中最對立的一個,它的主要特點不是根據(jù)用戶程序調(diào)用生成一個新的實例,而是控制某個類型的實例唯一性,通過上圖我們知道它包含的角色只有一個,就是Singleton,它擁有一個私有構(gòu)造函數(shù),這確保用戶無法通過new直接實例它。除此之外,該模式中包含一個靜態(tài)私有成員變量instance與靜態(tài)公有方法Instance()。Instance()方法負責檢驗并實例化自己,然后存儲在靜態(tài)成員變量中,以確保只有一個實例被創(chuàng)建。

圖2單例模式(Singleton)邏輯模型
接下來我們將介紹6中不同的單例模式(Singleton)的實現(xiàn)方式。這些實現(xiàn)方式都有以下的共同點:
1.有一個私有的無參構(gòu)造函數(shù),這可以防止其他類實例化它,而且單例類也不應該被繼承,如果單例類允許繼承那么每個子類都可以創(chuàng)建實例,這就違背了Singleton模式“唯一實例”的初衷。
2.單例類被定義為sealed,就像前面提到的該類不應該被繼承,所以為了保險起見可以把該類定義成不允許派生,但沒有要求一定要這樣定義。
3.一個靜態(tài)的變量用來保存單實例的引用。
4.一個公有的靜態(tài)方法用來獲取單實例的引用,如果實例為null即創(chuàng)建一個。
版本一線程不安全
/// <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()); }
}
}
以上的實現(xiàn)方式適用于單線程環(huán)境,因為在多線程的環(huán)境下有可能得到Singleton類的多個實例。假如同時有兩個線程去判斷
(null == _singleton),并且得到的結(jié)果為真,那么兩個線程都會創(chuàng)建類Singleton的實例,這樣就違背了Singleton模式“唯一實例”的初衷。
版本二線程安全
/// <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());
}
}
}
}
以上方式的實現(xiàn)方式是線程安全的,首先我們創(chuàng)建了一個靜態(tài)只讀的進程輔助對象,由于lock是確保當一個線程位于代碼的臨界區(qū)時,另一個線程不能進入臨界區(qū)(同步操作)。如果其他線程試圖進入鎖定的代碼,則它將一直等待,直到該對象被釋放。從而確保在多線程下不會創(chuàng)建多個對象實例了。只是這種實現(xiàn)方式要進行同步操作,這將是影響系統(tǒng)性能的瓶頸和增加了額外的開銷。
Double-Checked Locking
前面講到的線程安全的實現(xiàn)方式的問題是要進行同步操作,那么我們是否可以降低通過操作的次數(shù)呢?其實我們只需在同步操作之前,添加判斷該實例是否為null就可以降低通過操作的次數(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;
}
}
}
在介紹第四種實現(xiàn)方式之前,首先讓我們認識什么是,當字段被標記為beforefieldinit類型時,該字段初始化可以發(fā)生在任何時候任何字段被引用之前。這句話聽起了有點別扭,接下來讓我們通過具體的例子介紹。
/// <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;
}
}
上面我們定義了一個包含靜態(tài)字段和方法的類Test,但要注意我們并沒有定義靜態(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類添加一個靜態(tài)的構(gòu)造函數(shù)。

圖4 Test類的IL代碼
通過上面Test類的IL代碼的區(qū)別我們發(fā)現(xiàn),當Test類包含靜態(tài)字段,而且沒有定義靜態(tài)的構(gòu)造函數(shù)時,該類會被標記為beforefieldinit。
現(xiàn)在也許有人會問:“被標記為beforefieldinit和沒有標記的有什么區(qū)別呢”?OK現(xiàn)在讓我們通過下面的具體例子看一下它們的區(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é)果
接著我們在Main()方法中添加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é)果
通過上面的輸出結(jié)果,大家可以發(fā)現(xiàn)靜態(tài)字段的初始化跑到了靜態(tài)方法調(diào)用之前,Wo難以想象啊!
最后我們在Test類中添加一個靜態(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應該發(fā)生在”Echo!”之后和”After echo”之前,但這里卻出現(xiàn)了不唯一的結(jié)果,只有當Test類包含靜態(tài)構(gòu)造函數(shù)時,才能確保type initializer的初始化發(fā)生在”Echo!”之后和”After echo”之前。
所以說要確保type initializer發(fā)生在被字段引用時,我們應該給該類添加靜態(tài)構(gòu)造函數(shù)。接下來讓我們介紹單例模式的靜態(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;
}
}
}
以上方式實現(xiàn)比之前介紹的方式都要簡單,但它確實是多線程環(huán)境下,C#實現(xiàn)的Singleton的一種方式。由于這種靜態(tài)初始化的方式是在自己的字段被引用時才會實例化。
讓我們通過IL代碼來分析靜態(tài)初始化。

圖8靜態(tài)初始化IL代碼
首先這里沒有beforefieldinit的修飾符,由于我們添加了靜態(tài)構(gòu)造函數(shù)當靜態(tài)字段被引用時才進行初始化,因此即便很多線程試圖引用_instance,也需要等靜態(tài)構(gòu)造函數(shù)執(zhí)行完并把靜態(tài)成員_instance實例化之后可以使用。
延遲初始化
/// <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類中的一個靜態(tài)成員來完成,這樣就實現(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()
{
}
}
這種方式的簡單和性能良好,而且還提供檢查是否已經(jīng)創(chuàng)建實例的屬性IsValueCreated。
具體例子
現(xiàn)在讓我們使用單例模式(Singleton)實現(xiàn)負載平衡器,首先我們定義一個服務器類,它包含服務器名和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; }
}
由于負載平衡器只提供一個對象實例供服務器使用,所以我們使用單例模式(Singleton)實現(xiàn)該負載平衡器。
/// <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];
}
}
}
上面負載平衡器類LoadBalancer我們使用靜態(tài)初始化方式實現(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)點:
單例模式(Singleton)會控制其實例對象的數(shù)量,從而確保訪問對象的唯一性。
1.實例控制:單例模式防止其它對象對自己的實例化,確保所有的對象都訪問一個實例。
2.伸縮性:因為由類自己來控制實例化進程,類就在改變實例化進程上有相應的伸縮性。
單例模式的缺點:
1.系統(tǒng)開銷。雖然這個系統(tǒng)開銷看起來很小,但是每次引用這個類實例的時候都要進行實例是否存在的檢查。這個問題可以通過靜態(tài)實例來解決。
2.開發(fā)混淆。當使用一個單例模式的對象的時候(特別是定義在類庫中的),開發(fā)人員必須要記住不能使用new關鍵字來實例化對象。因為開發(fā)者看不到在類庫中的源代碼,所以當他們發(fā)現(xiàn)不能實例化一個類的時候會很驚訝。
3.對象生命周期。單例模式?jīng)]有提出對象的銷毀。在提供內(nèi)存管理的開發(fā)語言(比如,基于.NetFramework的語言)中,只有單例模式對象自己才能將對象實例銷毀,因為只有它擁有對實例的引用。在各種開發(fā)語言中,比如C++,其它類可以銷毀對象實例,但是這么做將導致單例類內(nèi)部的指針指向不明。
單例適用性
使用Singleton模式有一個必要條件:在一個系統(tǒng)要求一個類只有一個實例時才應當使用單例模式。反之,如果一個類可以有幾個實例共存,就不要使用單例模式。
不要使用單例模式存取全局變量。這違背了單例模式的用意,最好放到對應類的靜態(tài)成員中。
不要將數(shù)據(jù)庫連接做成單例,因為一個系統(tǒng)可能會與數(shù)據(jù)庫有多個連接,并且在有連接池的情況下,應當盡可能及時釋放連接。Singleton模式由于使用靜態(tài)成員存儲類實例,所以可能會造成資源無法及時釋放,帶來問題。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
C# Entity Framework中的IQueryable和IQueryProvider詳解
這篇文章主要介紹了C# Entity Framework中的IQueryable和IQueryProvider詳解,本文使用實例分析這兩個接口的內(nèi)部實現(xiàn),需要的朋友可以參考下2015-01-01
C# 創(chuàng)建、部署和調(diào)用WebService簡單示例
這篇文章主要為大家詳細介紹了C# 創(chuàng)建、部署和調(diào)用WebService的簡單示例,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-05-05
詳解WPF如何在Panel中實現(xiàn)設置所有子項間距
這篇文章主要為大家詳細介紹了WPF如何在Panel中實現(xiàn)設置所有子項間距,本文借鑒了 Qt 中的 Spacing 設置方法,感興趣的小伙伴可以跟隨小編一起學習一下2024-10-10

