原地址:http://www.cnblogs.com/BoyXiao/archive/2010/05/07/1729376.html 首先來明確一個問題,那就是在某些情況下,有些對象,我們只需要一個就可以了, 比如,一臺電腦上可以連好幾個印表機,但是這個電腦上的列印程式只能有一個, 這裡就可以通 ...
單例模式(Singleton)
原地址:http://www.cnblogs.com/BoyXiao/archive/2010/05/07/1729376.html
首先來明確一個問題,那就是在某些情況下,有些對象,我們只需要一個就可以了,
比如,一臺電腦上可以連好幾個印表機,但是這個電腦上的列印程式只能有一個,
這裡就可以通過單例模式來避免兩個列印作業同時輸出到印表機中,
即在整個的列印過程中我只有一個列印程式的實例。
簡單說來,單例模式(也叫單件模式)的作用就是保證在整個應用程式的生命周期中,
任何一個時刻,單例類的實例都只存在一個(當然也可以不存在)。
下麵來看單例模式的結構圖(圖太簡單了)
從上面的類圖中可以看出,在單例類中有一個構造函數 Singleton ,
但是這個構造函數卻是私有的(前面是“ - ”符號),
然後在裡面還公開了一個 GetInstance()方法,
通過上面的類圖不難看出單例模式的特點,從而也可以給出單例模式的定義
單例模式保證一個類僅有一個實例,同時這個類還必須提供一個訪問該類的全局訪問點。
先來將 Singleton 寫出來再說
Singleton 類
namespace Singleton
{
public class Singleton
{
//定義一個私有的靜態全局變數來保存該類的唯一實例
private static Singleton singleton;
/// <summary>
/// 構造函數必須是私有的
/// 這樣在外部便無法使用 new 來創建該類的實例
/// </summary>
private Singleton()
{
}
/// <summary>
/// 定義一個全局訪問點
/// 設置為靜態方法
/// 則在類的外部便無需實例化就可以調用該方法
/// </summary>
/// <returns></returns>
public static Singleton GetInstance()
{
//這裡可以保證只實例化一次
//即在第一次調用時實例化
//以後調用便不會再實例化
if (singleton == null)
{
singleton = new Singleton();
}
return singleton;
}
}
}
客戶端代碼
using System;
namespace SingletonTest
{
class Program
{
static void Main(string[] args)
{
Singleton.Singleton singletonOne =
Singleton.Singleton.GetInstance();
Singleton.Singleton singletonTwo =
Singleton.Singleton.GetInstance();
if (singletonOne.Equals(singletonTwo))
{
Console.WriteLine("singletonOne 和 singletonTwo 代表的是同一個實例");
}
else
{
Console.WriteLine("singletonOne 和 singletonTwo 代表的是不同一個實例");
}
Console.ReadKey();
}
}
}
運行結果為
從上面的結果可以看出來,儘管我兩次訪問了 GetInstance(),但是我訪問的只是同一個實例,
換句話來說,上面的代碼中,由於構造函數被設置為 private 了,
所以您無法再在 Singleton 類的外部使用 new 來實例化一個實例,您只能通過訪問 GetInstance()來訪問 Singleton 類,
GetInstance()通過如下方式保證該 Singleton 只存在一個實例:
首先這個 Singleton 類會在在第一次調用 GetInstance()時創建一個實例,並將這個實例的引用封裝在自身類中,
然後以後調用 GetInstance()時就會判斷這個 Singleton 是否存在一個實例了,如果存在,則不會再創建實例。
而是調用以前生成的類的實例,這樣下來,整個應用程式中便就只存在一個實例了。
從這裡再來總結單例模式的特點:
首先,單例模式使類在程式生命周期的任何時刻都只有一個實例,
然後,單例的構造函數是私有的,外部程式如果想要訪問這個單例類的話,
必須通過 GetInstance()來請求(註意是請求)得到這個單例類的實例。
有的時候,總是容易把全局變數和單例模式給弄混了,下麵就剖析一下全局變數和單例模式相比的缺點
首先,全局變數呢就是對一個對象的靜態引用,全局變數確實可以提供單例模式實現的全局訪問這個功能,
但是,它並不能保證您的應用程式中只有一個實例,同時,在編碼規範中,也明確指出,
應該要少用全局變數,因為過多的使用全局變數,會造成代碼難讀,
還有就是全局變數並不能實現繼承(雖然單例模式在繼承上也不能很好的處理,但是還是可以實現繼承的)
而單例模式的話,其在類中保存了它的唯一實例,這個類,它可以保證只能創建一個實例,
同時,它還提供了一個訪問該唯一實例的全局訪問點。
上面呢,差不多就將單例模式的核心給介紹完了,
或許,您會覺得單例模式就這麼個東西啊,不就是保證只有一個實例嘛,也太簡單了,
如果您真這麼想的話,那您就錯了,因為要保證在整個應用程式生命周期中保證只有一個實例不是那麼容易的,
下麵就來看一種情況(這裡先假設我的應用程式是多線程應用程式),同時還是以前面的 Demo 來做為說明,
如果在一開始調用 GetInstance()時,是由兩個線程同時調用的(這種情況是很常見的),註意是同時,
(或者是一個線程進入 if 判斷語句後但還沒有實例化 Singleton 時,第二個線程到達,此時 singleton 還是為 null)
這樣的話,兩個線程均會進入 GetInstance(),而後由於是第一次調用 GetInstance(),
所以存儲在 Singleton 中的靜態變數 singleton 為 null ,這樣的話,就會讓兩個線程均通過 if 語句的條件判斷,
然後調用 new Singleton()了,
public static Singleton GetInstance()
{
if (singleton == null)
{
singleton = new Singleton();
}
return singleton;
}
這樣的話,問題就出來了,因為有兩個線程,所以會創建兩個實例,
很顯然,這便違法了單例模式的初衷了,
那麼如何解決上面出現的這個問題(即多線程下使用單例模式時有可能會創建多個實例這一現象)呢?
其實,這個是很好解決的,
您可以這樣思考這個問題:
由於上面出現的問題中涉及到多個線程同時訪問這個 GetInstance(),
那麼您可以先將一個線程鎖定,然後等這個線程完成以後,再讓其他的線程訪問 GetInstance()中的 if 段語句,
比如,有兩個線程同時到達
如果 singleton != null 的話,那麼上面提到的問題是不會存在的,因為已經存在這個實例了,這樣的話,
所有的線程都無法進入 if 語句塊,
也就是所有的線程都無法調用語句 new Singleton()了,
這樣還是可以保證應用程式生命周期中的實例只存在一個,
但是如果此時的 singleton == null 的話,
那麼意味著這兩個線程都是可以進入這個 if 語句塊的,
那麼就有可能出現上面出現的單例模式中有多個實例的問題,
此時,我可以讓一個線程先進入 if 語句塊,然後我在外面對這個 if 語句塊加鎖,
對第二個線程呢,由於 if 語句進行了加鎖處理,所以這個進程就無法進入 if 語句塊而處於阻塞狀態,
當進入了 if 語句塊的線程完成 new Singleton()後,這個線程便會退出 if 語句塊,
此時,第二個線程就從阻塞狀態中恢復,即就可以訪問 if 語句塊了,但是由於前面的那個線程已近創建了 Singleton 的實例,
所以 singleton != null ,此時,第二個線程便無法通過 if 語句的判斷條件了,
即無法進入 if 語句塊了,這樣便保證了整個生命周期中只存在一個實例,
也就是只有第一個線程創建了 Singleton 實例,第二個線程則無法創建實例。
下麵就來重新改進前面 Demo 中的 Singleton 類,使其在多線程的環境下也可以實現單例模式的功能。
namespace Singleton
{
public class Singleton
{
//定義一個私有的靜態全局變數來保存該類的唯一實例
private static Singleton singleton;
//定義一個只讀靜態對象
//且這個對象是在程式運行時創建的
private static readonly object syncObject = new object();
/// <summary>
/// 構造函數必須是私有的
/// 這樣在外部便無法使用 new 來創建該類的實例
/// </summary>
private Singleton()
{
}
/// <summary>
/// 定義一個全局訪問點
/// 設置為靜態方法
/// 則在類的外部便無需實例化就可以調用該方法
/// </summary>
/// <returns></returns>
public static Singleton GetInstance()
{
//這裡可以保證只實例化一次
//即在第一次調用時實例化
//以後調用便不會再實例化
//第一重 singleton == null
if (singleton == null)
{
lock (syncObject)
{
//第二重 singleton == null
if (singleton == null)
{
singleton = new Singleton();
}
}
}
return singleton;
}
}
}
上面的就是改進後的代碼,可以看到在類中有定義了一個靜態的只讀對象 syncObject,
這裡需要說明的是,為何還要創建一個 syncObject 靜態只讀對象呢?
由於提供給 lock 關鍵字的參數必須為基於引用類型的對象,該對象用來定義鎖的範圍,
所以這個引用類型的對象總不能為 null 吧,而一開始的時候,singleton 為 null ,所以是無法實現加鎖的,
所以必須要再創建一個對象即 syncObject 來定義加鎖的範圍。
還有要解釋一下的就是在 GetInstance()中,我為什麼要在 if 語句中使用兩次判斷 singleton == null ,
這裡涉及到一個名詞 Double-Check Locking ,也就是雙重檢查鎖定,
為何要使用雙重檢查鎖定呢?
考慮這樣一種情況,就是有兩個線程同時到達,即同時調用 GetInstance(),
此時由於 singleton == null ,所以很明顯,兩個線程都可以通過第一重的 singleton == null ,
進入第一重 if 語句後,由於存在鎖機制,所以會有一個線程進入 lock 語句併進入第二重 singleton == null ,
而另外的一個線程則會在 lock 語句的外面等待。
而當第一個線程執行完 new Singleton()語句後,便會退出鎖定區域,此時,第二個線程便可以進入 lock 語句塊,
此時,如果沒有第二重 singleton == null 的話,那麼第二個線程還是可以調用 new Singleton()語句,
這樣第二個線程也會創建一個 Singleton 實例,這樣也還是違背了單例模式的初衷的,
所以這裡必須要使用雙重檢查鎖定。
細心的朋友一定會發現,如果我去掉第一重 singleton == null ,程式還是可以在多線程下完好的運行的,
考慮在沒有第一重 singleton == null 的情況下,
當有兩個線程同時到達,此時,由於 lock 機制的存在,第一個線程會進入 lock 語句塊,並且可以順利執行 new Singleton(),
當第一個線程退出 lock 語句塊時, singleton 這個靜態變數已不為 null 了,所以當第二個線程進入 lock 時,
還是會被第二重 singleton == null 擋在外面,而無法執行 new Singleton(),
所以在沒有第一重 singleton == null 的情況下,也是可以實現單例模式的?那麼為什麼需要第一重 singleton == null 呢?
這裡就涉及一個性能問題了,因為對於單例模式的話,new Singleton()只需要執行一次就 OK 了,
而如果沒有第一重 singleton == null 的話,每一次有線程進入 GetInstance()時,均會執行鎖定操作來實現線程同步,
這是非常耗費性能的,而如果我加上第一重 singleton == null 的話,
那麼就只有在第一次,也就是 singleton ==null 成立時的情況下執行一次鎖定以實現線程同步,
而以後的話,便只要直接返回 Singleton 實例就 OK 了而根本無需再進入 lock 語句塊了,這樣就可以解決由線程同步帶來的性能問題了。
好,關於多線程下單例模式的實現的介紹就到這裡了,但是,關於單例模式的介紹還沒完。
下麵將要介紹的是懶漢式單例和餓漢式單例
懶漢式單例
何為懶漢式單例呢,可以這樣理解,單例模式呢,其在整個應用程式的生命周期中只存在一個實例,
懶漢式呢,就是這個單例類的這個唯一實例是在第一次使用 GetInstance()時實例化的,
如果您不調用 GetInstance()的話,這個實例是不會存在的,即為 null
形象點說呢,就是你不去動它的話,它自己是不會實例化的,所以可以稱之為懶漢。
其實呢,我前面在介紹單例模式的這幾個 Demo 中都是使用的懶漢式單例,
看下麵的 GetInstance()方法就明白了:
public static Singleton GetInstance()
{
if (singleton == null)
{
lock (syncObject)
{
if (singleton == null)
{
singleton = new Singleton();
}
}
}
return singleton;
}
從上面的這個 GetInstance()中可以看出這個單例類的唯一實例是在第一次調用 GetInstance()時實例化的,
所以此為懶漢式單例。
餓漢式單例
上面介紹了餓漢式單例,到這裡來理解懶漢式單例的話,就容易多了,懶漢式單例由於人懶,
所以其自己是不會主動實例化單例類的唯一實例的,而餓漢式的話,則剛好相反,
其由於肚子餓了,所以到處找東西吃,人也變得主動了很多,所以根本就不需要別人來催他實例化單例類的為一實例,
其自己就會主動實例化單例類的這個唯一類。
在 C# 中,可以用特殊的方式實現餓漢式單例,即使用靜態初始化來完成餓漢式單例模式
下麵就來看一看餓漢式單例類
namespace Singleton
{
public sealed class Singleton
{
private static readonly Singleton singleton = new Singleton();
private Singleton()
{
}
public static Singleton GetInstance()
{
return singleton;
}
}
}
要先在這裡提一下的是使用靜態初始化的話,無需顯示地編寫線程安全代碼,
C# 與 CLR 會自動解決前面提到的懶漢式單例類時出現的多線程同步問題。
上面的餓漢式單例類中可以看到,當整個類被載入的時候,就會自行初始化 singleton 這個靜態只讀變數。
而非在第一次調用 GetInstance()時再來實例化單例類的唯一實例,所以這就是一種餓漢式的單例類。
好,到這裡,就真正的把單例模式介紹完了,在此呢再總結一下單例類需要註意的幾點:
一、單例模式是用來實現在整個程式中只有一個實例的。
二、單例類的構造函數必須為私有,同時單例類必須提供一個全局訪問點。
三、單例模式在多線程下的同步問題和性能問題的解決。
四、懶漢式和餓漢式單例類。
五、C# 中使用靜態初始化實現餓漢式單例類。