Singleton通常被認為是最簡單的設計模式,很多初學者都是通過它來了解設計模式的含義。然而,熟悉設計模式的技術人員都知道,要正确實作Singleton模式實際上是非常難的,涉及到很多技術細節。本文對于Singleton做了大膽深入的研究,并且探讨了C++、Java和C#中的Singleton實作。
在開發軟體應用程式過程中,随着應用程式的開發,會出現重複性的模式。随着整個軟體系統的開發,很多相同的模式會逐漸顯現出來。
這種重複性模式概念在其他應用中是非常明顯的。汽車制造就是一種此類應用。很多不同的汽車型号使用相同的子構件,包括大多數基本部件(例如,燈泡和緊固零件)以及較大的構件(例如,底盤和發動機)。
在住宅建築中,重複性模式概念适用于螺絲和螺釘以及整體總體建築物配電系統。無論組建的小組是為了開發新的汽車設計還是新的建築物設計,它通常不必沒有考慮到以前已解決的問題。如果設計和建築住宅的小組必須重新構思和設計房子的每一個組成部分,則整個過程所花的時間比現在要長得多。門高或燈開關功能等許多設計決策(例如,門高或燈開關功能)很容易了解。房為滿足給房子不同部分提供洗手功能的要求,房屋設計師不必重新設計和重建立造不同類型的輸供水和蓄水設施,以便達到為房子不同部分提供洗手功能的要求:标準水槽以及标準的熱水和冷水輸入接頭和排水輸出接頭是很容易了解非常常見的房屋建築構件。可以将重複性模式概念反複應用于我們周圍的幾乎每樣東西上,包括軟體。
汽車和住宅建築示例有助于在軟體設計和構造中展現某些一般性的抽象概念。易于了解且明确定義的通用功能部件的概念是設計模式的源動力,它也是其他兩篇設計模式文章探究工廠設計模式和探究觀察者設計模式的重點。這些模式幾乎涵蓋了面向對象的軟體設計的各個方面,包括對象建立、對象互動和對象生存期。在本文中,我們将讨論Singleton模式,它包含在創造性模式系列中。
創造性模式訓示如何以及何時建立對象。很多執行個體需要隻能通過創造性方法解決的特殊行為,而不是在建立執行個體後強制實施所需的行為。此類行為要求最好的例子之一包含在Singleton模式中。Singleton模式在《設計模式:可複用的面向對象軟體的基礎》這一經典參考書目中有正式的定義,該書的作者包括Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides(也稱為四人組或GoF)。在設計模式中,此模式是最簡單也是使用最廣泛的模式之一。但是,正如我們将會看到的一樣,在實作此模式時可能會出現一些問題。本文試圖通過Singleton模式的多個早期實作來從頭開始分析Singleton模式,以及如何在Microsoft .NET應用程式開發中發揮其最佳用途。
Singleton模式
按照設計模式中的定義,Singleton模式的用途是“ensure a class has only one instance, and provide a global point of access to it(確定每個類隻有一個執行個體,并提供它的全局通路點)”。
它可以解決什麼問題,或者換句話說,我們使用它的動機是什麼?幾乎在每個應用程式中,都需要有一個從中進行全局通路和維護某種類型資料的區域。在面向對象的(OO)系統中也有這種情況,在此類系統中,在任何給定時間隻應運作一個類或某個類的一組預定義數量的執行個體。例如,當使用某個類來維護增量計數器時,此簡單的計數器類需要跟蹤在多個應用程式領域中使用的整數值。此類需要能夠增加該計數器并傳回目前的值。對于這種情況,所需的類行為應該僅使用一個類執行個體來維護該整數,而不是使用其它類執行個體來維護該整數。
最初,人們可能會試圖将計數器類執行個體隻作為靜态全局變量來建立。這是一種通用的方法,但實際上隻解決一部分問題;它解決了全局可通路性問題,但沒有采取任何措施來確定在任何給定的時間隻運作一個類執行個體。應該由類本身來負責隻使用一個類執行個體,而不是由類使用者來負責。應該始終不要讓類使用者來監視和控制運作的類執行個體的數量。
所需要的是使用某種方法來控制如何建立類執行個體,然後確定在任何給定的時間隻建立一個類執行個體。這會确切地給我們提供所需的行為,并使用戶端不必了解任何類細節。
邏輯模型
Singleton模型非常簡單直覺。(通常)隻有一個Singleton執行個體。用戶端通過一個已知的通路點來通路Singleton執行個體。在這種情況下,用戶端是一個需要通路唯一Singleton執行個體的對象。圖1以圖形方式顯示此關系。
實體模型
Singleton模式的實體模型也是非常簡單的。但是,随着時間的推移,實作Singleton的方式也略有不同。讓我們看一下原始的GoFSingleton實作。圖2顯示按設計模式所定義的原始Singleton模式的UML模型。
我們看到的是一個簡單的類圖表,顯示有一個Singleton對象的私有靜态屬性以及傳回此相同屬性的公共方法Instance()。這實際上是Singleton的核心。還有其他一些屬性和方法,用于說明在該類上允許執行的其他操作。為了便于此次讨論,讓我們将重點放在執行個體屬性和方法上。
用戶端僅通過執行個體方法來通路任何Singleton執行個體。此處沒有定義建立執行個體的方式。我們還希望能夠控制如何以及何時建立執行個體。在OO開發中,通常可以在類的構造函數中最好地處理特殊對象的建立行為。這種情況也不例外。我們可以做的是,定義我們何時以及如何構造類執行個體,然後禁止任何用戶端直接調用該構造函數。這是在Singleton構造中始終使用的方法。讓我們看一下設計模式中的原始示例。通常,将下面所示的C++Singleton示例實作代碼示例視為Singleton的預設實作。本示例已移植到很多其他程式設計語言中,通常它在任何地方的形式與此幾乎相同。
C++Singleton示例實作代碼
//Declaration
class Singleton{
public:
static Singleton* Instance();
protected:
Singleton();
private:
static Singleton* _instance;
}
// Implementation
Singleton* Singleton::_instance = 0;
Singleton* Singleton::Instance() {
if (_instance == 0) {
_instance = new Singleton;
return _instance;
讓我們先花點時間分析一下此代碼。該簡單類有一個成員變量,此變量是指向該類自身的指針。注意,構造函數是受保護的,并且隻有公共方法才是執行個體方法。在執行個體方法實作中,有一個控制塊(if),它檢查成員變量是否已初始化,如果沒有的話,則建立一個新執行個體。控制塊中這種惰性初始化意味着僅在第一次調用Instance()方法時初始化或建立Singleton執行個體。對于很多應用程式,這種方法效果很好。但對于多線程應用程式,這種方法證明具有潛在危險的副作用。如果兩個線程同時進入控制塊,則可能會建立該成員變量的兩個執行個體。要解決這一問題,您可能想隻将重要部分放在控制塊周圍以確定線程安全。如果您這樣做,則将對執行個體方法的所有調用進行序列化處理,并且可能會對性能産生不利影響(取決于應用程式)。正是由于這個原因,建立了此模式的另一個版本,它使用某種稱為雙重檢驗機制的功能。下一個代碼示例顯示使用Java文法的雙重檢驗鎖定。
使用Java文法的雙重檢驗鎖定Singleton代碼
//C++ port to Java
class Singleton
{
public staticSingletonInstance() {
if (_instance == null) {
synchronized (Class.forName("Singleton")) {
_instance = new Singleton();
protected Singleton() {}
private staticSingleton_instance = null;
在使用Java文法的雙重檢驗鎖定Singleton代碼示例中,我們直接将C++代碼移植到Java代碼,以便利用Java關鍵部分塊(已同步)。主要差别是不再有單獨的聲明和實作部分,沒有指針資料類型,并且采用了新的雙重檢驗機制。雙重檢驗發生在第一個IF塊上。如果成員變量為空,則執行進入關鍵部分塊,該塊再次雙重檢驗該成員變量。僅在通過此最終測試後,才會執行個體化該成員變量。一般來說,兩個線程無法使用這種方法建立兩個類執行個體。另外,因為在第一次檢查時沒有出現線程阻塞,是以對此方法的大多數調用不會由于必須進入鎖定而導緻性能下降。目前,在實作Singleton模式時,很多Java應用程式中都廣泛使用這種方法。這種方法很巧妙,但也有瑕疵。某些優化編譯器可以将惰性初始化代碼優化掉或對其重新進行排序,并且會重新産生線程安全問題。有關更深入的解釋,請參閱"The Double-Check Locking is Broken" (http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html)。
另一種試圖解決此問題的方法可能是,在成員變量聲明中使用volatile關鍵字。這應該告訴編譯器不要對代碼重新排序,并且放棄優化。目前,這是唯一建議的JVM記憶體模型,并且不會立即解決該問題。
實作Singleton的最好方法是什麼?最終(而不是碰巧),Microsoft .NET架構解決了所有這些問題,進而更易于實作Singleton,卻不會産生我們目前讨論的不利副作用。.NET架構以及C#語言允許我們在必要時通過替換語言關鍵字,将上述的Java文法移植到C#文法。是以,Singleton代碼變為以下内容:
以C#編碼的雙重檢驗鎖定
// Port to C#
lock (typeof(Singleton)) {
private static volatileSingleton_instance = null;
此處,我們替換了鎖定關鍵字來執行關鍵部分塊,使用typeof操作并添加volatile關鍵字,以確定沒有對代碼進行優化程式重新排序。雖然此代碼或多或少是GoFSingleton模式的直接移植,但它可達到我們的目的,并且我們可獲得所需的行為。此代碼還說明了将C++移植到Java和将Java移植到C#代碼的一些相似之處和主要差别。但是,正如任何代碼移植一樣,通常目智語言或平台的一些優點可能在移植過程中失去。需要做的就是對代碼重構,以便利用新目智語言或平台的功能。
在前面的每個代碼示例中,Singleton的原始實作随時間的推移而發生變化,以解決在每個新模式實作中發現的問題。一些問題(例如,線程安全)要求對大多數實作進行更改,以滿足在目前應用程式中日益增長的需要并解決演變發展問題。.NET在應用程式開發中提供了一個演變步驟。可以在“架構”級别解決前面示例中出現的很多亟待解決的問題,而不是在實作級别解決。雖然上一個示例顯示了一個使用.NET架構和C#的有效Singleton類,但隻需更好地利用.NET架構本身就可以大大簡化此代碼。以下示例使用.NET,它是一個松散地基于原始GoF模式的最小限度的Singleton類,并且仍然可獲得類似的行為。
.NETSingleton示例
//.NET Singleton
sealed class Singleton
private Singleton() {}
public static readonlySingletonInstance = new Singleton();
此版本已大大簡化并且更加直覺。它仍然是Singleton嗎?讓我們看一下更改了哪些内容,然後再做決定。我們修改了要密封的類本身(該類密封後是不可繼承的),删除了惰性初始化代碼,删除了Instance()方法,并且對_instance變量做了大量的修改。對_instance變量所做的更改包括修改對公共方法的通路級别,将變量标記為隻讀,以及在聲明時初始化該變量。此處,我們可以直接定義所需的行為,而不關心實作的潛在有害的副作用。那麼,使用惰性初始化有什麼優點以及使用多個線程有什麼危險呢?在.NET架構中内置了所有正确的行為。讓我們先看第一種情況:惰性初始化。
最初使用惰性初始化的主要原因是要擷取僅在第一次調用Instance()方法中建立執行個體的行為,還因為C++規範中具有某種開放性,并不定義靜态變量的确切初始化順序。要在C++中獲得所需的Singleton行為,必須采用涉及使用惰性初始化的運算方法。我們真正關心的是在第一次(在該情況下)調用執行個體屬性中建立該執行個體,還是在此調用之前建立該執行個體的,并且類中的靜态變量是否有已定義的初始化順序。對于.NET架構,這就是我們擷取的行為。在JIT過程中,當(且僅當)任何方法使用靜态屬性時,“架構”将初始化此靜态屬性。如果沒有使用該屬性,則不會建立執行個體。更準确地說,在JIT過程中發生的事情就是,在任何調用方使用該類的任何靜态成員時構造和加載該類。在這種情況下,結果是相同的。
那麼,線程安全初始化呢?“架構”也解決了這一問題。“架構”内部保證靜态類型初始化的線程安全。換句話說,在上面的示例中,隻建立一個Singleton類執行個體。還要注意,用于儲存類執行個體的屬性字段稱為執行個體。此選項更好地說明了,在本文中的讨論過程中,此值是類的執行個體。在“架構”本身中,雖然使用的屬性名稱稱為值,但有多個類使用此類型的Singleton。概念完全相同。
對類所做的其他更改意味着禁止劃分子類。添加密封類修飾符可確定不會将該類劃分為子類。GoFSingleton模式詳細介紹了試圖對Singleton劃分子類所産生的問題,該劃分通常并不是小事。在大多數情況下,可以很容易地開發沒有父類的Singleton,并且添加劃分子類功能會增加通常根本不需要的新的複雜性級别。随着複雜性的提高,測試、教育訓練和文檔編制等所需的時間也會增加。通常,除非絕對必要,否則您不希望提高任何代碼的複雜性。
讓我們看一下如何使用Singleton。使用我們最初的計數器的有關動機的概念,我們可以建立一個簡單的Singleton計數器類并說明我們将如何使用它。圖3顯示了UML類說明将包含什麼内容。
相應的類實作代碼以及示例用戶端使用如下所示。
示例Singleton使用
sealed class SingletonCounter {
public static readonly SingletonCounter Instance =
new SingletonCounter();
private long Count = 0;
private SingletonCounter() {}
public long NextValue() {
return ++Count;
class SingletonClient {
[STAThread]
static void Main() {
for (int i=0; i<20; i++) {
Console.WriteLine("NextSingletonvalue: {0}",
SingletonCounter.Instance.NextValue());
此處,我們還建立了一個Singleton類來維護具有long類型的增量計數。用戶端是一個簡單的控制台應用程式,它顯示計數器類的20個值。雖然此示例極其簡單,但它卻說明了如何使用.NET來實作Singleton,然後将其用在應用程式中。
小結
Singleton設計模式是一個非常有用的機制,可用于在面向對象的應用程式中提供單個對象通路點。無論使用的是什麼實作,該模式提供一個大家所熟知的概念,以便其在設計和開發小組之間友善地進行共享。但是,正如我們所發現的一樣,注意到這些實作有多大差異及其潛在的副作用也是非常重要的。.NET架構為模式實作者在設計所需的功能類型方面提供了很大的幫助,實作者無需處理本文中所讨論的很多副作用。在正确實作後,可以證明模式的最初目的的有效性。
設計模式是非常有用的軟體設計概念,可使小組将重點放在提供最佳類型的應用程式上,而不考慮它們是什麼應用程式。關鍵在于正确而有效地使用設計模式,目前有很多關于将設計模式用于Microsoft .NET方面的MSDN系列文檔,其中介紹了如何正确而有效地使用設計模式。