首先來明確一個問題,那就是在某些狀況下,有些對象,咱們只須要一個就能夠了,html
好比,一臺計算機上能夠連好幾個打印機,可是這個計算機上的打印程序只能有一個,安全
這裏就能夠經過單例模式來避免兩個打印做業同時輸出到打印機中,多線程
即在整個的打印過程當中我只有一個打印程序的實例。函數
簡單說來,單例模式(也叫單件模式)的做用就是保證在整個應用程序的生命週期中,post
任何一個時刻,單例類的實例都只存在一個(固然也能夠不存在)。性能
下面來看單例模式的結構圖(圖太簡單了)編碼
從上面的類圖中能夠看出,在單例類中有一個構造函數 Singleton ,url
可是這個構造函數倒是私有的(前面是「 - 」符號),spa
而後在裏面還公開了一個 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()這樣的話,問題就出來了,由於有兩個線程,因此會建立兩個實例,
很顯然,這便違法了單例模式的初衷了,
那麼如何解決上面出現的這個問題(即多線程下使用單例模式時有可能會建立多個實例這一現象)呢?
其實,這個是很好解決的,
您能夠這樣思考這個問題:
因爲上面出現的問題中涉及到多個線程同時訪問這個 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()時再來實例化單例類的惟一實例,因此這就是一種餓漢式的單例類。
好,到這裏,就真正的把單例模式介紹完了,在此呢再總結一下單例類須要注意的幾點:
1、單例模式是用來實如今整個程序中只有一個實例的。
2、單例類的構造函數必須爲私有,同時單例類必須提供一個全局訪問點。
3、單例模式在多線程下的同步問題和性能問題的解決。
4、懶漢式和餓漢式單例類。
5、C# 中使用靜態初始化實現餓漢式單例類。