介紹
單例模式(Singleton)保證一個類僅有一個實例,并提供一個訪問它的全局訪問點。
單例模式的結(jié)構(gòu)圖如下所示:
使用單例模式的原因
對一些類來說,只有一個實例是很重要的。如何才能保證一個類只有一個實例并且這個實例易于被訪問呢?
基于程序員之間的約定或是利用全局變量嗎?
雖然這樣或許也可以實現(xiàn),但是單例模式確是更好的做法。因為僅僅靠約定或是全局變量,如果重新new一個對象,還是可以創(chuàng)建新的實例。
經(jīng)典的單例模式
現(xiàn)在我們先來看看經(jīng)典的單例模式寫法:
?public class Singleton
{
? ? ?private static Singleton? _instance;
? ? ?private Singleton() { }
? ? ?public static Singleton GetInstance()
? ? {
? ? ? ? ?if(_instance == null)
? ? ? ? ? ?_instance = new Singleton();
? ? ? ? return _instance;
? ? }
}
private static Singleton? _instance;
聲明了一個靜態(tài)的類變量,靜態(tài)類變量在類的所有實例之間共享, 與類關聯(lián)而不是與類的實例關聯(lián),它在整個應用程序域中只有一個實例。
private Singleton() { }
私有的構(gòu)造函數(shù),這使得我們無法通過:
Singleton Singleton = new Singleton();
這種寫法來創(chuàng)建它的實例,這樣寫會報錯,如下所示:
?public static Singleton GetInstance()
? ? {
? ? ? ? ?if(_instance == null)
? ? ? ? ? ?_instance = new Singleton();
? ? ? ? return _instance;
? ? }
該方法是獲得本類實例的唯一全局訪問點。
如果實例不存在,在類的內(nèi)部可以通過new操作符來獲取一個實例,否則返回已有的實例。
internal class Program
{
? ? static void Main(string[] args)
? ? { ? ? ? ?
? ? ? ? Singleton singleton1 = Singleton.GetInstance();
? ? ? ? Singleton singleton2 = Singleton.GetInstance();
? ? ? ? if (singleton1 == singleton2)
? ? ? ? ? ? Console.WriteLine("兩個對象是相同的實例");
? ? }
}
比較兩個對象看看它們是不是同一個實例,運行結(jié)果如下所示:
多線程下的單例模式
上述代碼在多線程情況下會存在問題,如果有多個線程同時訪問Singleton類調(diào)用GetInstance方法,會有可能造成創(chuàng)建多個實例。
查看以下代碼:
internal class Program
{
? ?static void Main(string[] args)
? { ? ? ? ?
? ? ? ?// 創(chuàng)建并啟動兩個任務
? ? ? ?Task task1 = Task.Factory.StartNew(Method1);
? ? ? ?Task task2 = Task.Factory.StartNew(Method2);
? ? ? ?Task.WaitAll(task1, task2); ? ? ? ?
? ? ? ?Console.WriteLine("所有任務完成");
? ? ? ?void Method1()
? ? ? {
? ? ? ? ? ?Console.WriteLine("Task 1 is running.");
? ? ? ? ? ?Task.Delay(1000).Wait();
? ? ? ? ? ?Singleton singleton1 = Singleton.GetInstance();
? ? ? ? ? ?Console.WriteLine(singleton1.GetHashCode());
? ? ? }
?
? ? ? ? void Method2()
? ? ? { ? ? ? ?
? ? ? ? ? ?Console.WriteLine("Task 2 is running.");
? ? ? ? ? ?Task.Delay(1000).Wait();
? ? ? ? ? ?Singleton singleton2 = Singleton.GetInstance();
? ? ? ? ? ?Console.WriteLine(singleton2.GetHashCode());
? ? ? }
? }
}
運行結(jié)果如下所示:
兩個對象的哈希碼不同,在C#中,每個對象都有一個GetHashCode
方法,該方法返回該對象的哈希碼。默認情況下,GetHashCode
方法是根據(jù)對象的內(nèi)存地址生成的,因此兩個不同的實例在內(nèi)存中有不同的地址,它們的哈希碼通常也是不同的。然而,這并不是說哈希碼不同就一定表示兩個對象是不同的。因為哈希碼是一個有限的整數(shù),存在哈希沖突的可能性。兩個不同的對象可能具有相同的哈希碼,這被稱為哈希沖突。因此,不能僅僅通過比較哈希碼就斷定兩個對象是相同的還是不同的。
但是我們在這里我們可以這樣進行簡單的判斷。
那么該如何解決這個問題呢?
使用lock
C#中可以使用lock來解決。 lock
語句可確保在任何時候最多只有一個線程執(zhí)行其主體。
可以這樣進行改寫:
public class Singleton
{
? ? private static Singleton? _instance;
? ? private static readonly object _syncRoot = new object();
? ? private Singleton() { }
? ? public static Singleton GetInstance()
? ? {
? ? ? ? lock (_syncRoot)
? ? ? ? {
? ? ? ? ? ? if (_instance == null)
? ? ? ? ? ? ? ? _instance = new Singleton();
? ? ? ? }
? ? ? ?return _instance;
? ? }
}
private static readonly object _syncRoot = new object();
程序運行時創(chuàng)建一個靜態(tài)只讀的輔助對象。
?public static Singleton GetInstance()
? ? {
? ? ? ? lock (_syncRoot)
? ? ? ? {
? ? ? ? ? ? if (_instance == null)
? ? ? ? ? ? ? ? _instance = new Singleton();
? ? ? ? }
? ? ? ?return _instance;
? ? }
lock語句確保在任何時候最多只有一個線程執(zhí)行其主體。
現(xiàn)在再來看看運行結(jié)果:
雙重鎖定
現(xiàn)在兩個對象的哈希碼一樣了,在這里我們可以簡單的認為是同一個實例了。
但是每次都要執(zhí)行l(wèi)ock語句會影響性能,還需要改進:
?public class Singleton
{
? ? ?private static Singleton? _instance;
? ? ?private static readonly object _syncRoot = new object();
? ? ?private Singleton() { }
? ? ?public static Singleton GetInstance()
? ? {
? ? ? ? ?if (_instance == null)
? ? ? ? {
? ? ? ? ? ? ?lock (_syncRoot)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? ?if(_instance == null)
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? ?_instance = new Singleton();
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? } ? ? ? ? ? ? ?
? ? ? ? return _instance;
? ? }
}
這樣子當存在_instance
時,直接返回,沒有執(zhí)行l(wèi)ock語句。為什么執(zhí)行兩次if(_instance == null)
判斷是因為當_instance == null
時,如果同時有兩個線程調(diào)用GetInstance
方法,它們都可以通過第一次判斷,由于lock
的機制,這兩個線程只有一個進去,另一個在外排隊等候,必須等上一個進入并且退出之后,第二個線程才能進入,而此時如果沒有第二層的判斷,那么第一個線程與第二個線程都會創(chuàng)建新的實例,而添加了這個判斷之后,第一個線程創(chuàng)建了實例,_instanc就不為空,第二個線程就無法創(chuàng)建新的實例了。
現(xiàn)在再來看看運行結(jié)果:
靜態(tài)初始化
其實在實際應用中,C#也可以采用靜態(tài)初始化的方法,這種方法不需要開發(fā)人員顯式地編寫線程安全代碼,即可解決多線程環(huán)境下不安全的問題。
public sealed class Singleton
{
? ? private static readonly Singleton _instance = new Singleton(); ? ?
? ? private Singleton() { }
? ? public static Singleton GetInstance()
? ? { ? ? ? ?
? ? ? return _instance;
? ? }
}
使用sealed
關鍵字表示是密封類,阻止發(fā)生派生,因為派生可能會增加實例。
private static readonly Singleton _instance = new Singleton(); ? ?
在第一次引用類的任何成員時創(chuàng)建實例,公共語言運行時負責處理變量初始化。
現(xiàn)在再來看看運行結(jié)果:
由于這種靜態(tài)初始化的方式是在自己被加載時就將自己實例化,所以被形象地稱之為餓漢單例類,原先的單例模式的處理方式是要在第一次被引用時,才會將自己實例化,所以就被稱為懶漢單例類。
總結(jié)
本文介紹了在C#中如何使用單例模式,并介紹了在多線程模式下單例模式可能存在的問題及其解決方法,希望對你有所幫助。
參考
1、《Head First 設計模式(中文版)》
2、《大話設計模式》文章來源:http://www.zghlxwxcb.cn/news/detail-777750.html
3、《設計模式:可復用面向?qū)ο筌浖幕A》文章來源地址http://www.zghlxwxcb.cn/news/detail-777750.html
到了這里,關于C#設計模式之單例模式的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!