上下文
您要在 C# 中構建應用程式。您需要只有一個執行個體的類,並且需要提供一個用於訪問執行個體的全域訪問點。您希望確保您的解決方案高效,並且能夠利用 Microsoft? .NET 公用語言運行庫功能。您可能還希望確保解決方案是安全執行緒的。
實現策略
儘管 Singleton 是一種相對簡單的模式,但是存在與具體實現有關的不同權衡因素和選項。下面是一組實現策略,及其優缺點的討論。
Singleton
Singleton 設計模式的下列實現採用了 Design Patterns: Elements of Reusable Object-Oriented Software [Gamma95] 中所描述的解決方案,但對它進行了修改,以便利用 C# 中可用的語言功能,如屬性:
using System;public class Singleton{private static Singleton instance;private Singleton() {}public static Singleton Instance{get{if (instance == null){instance = new Singleton();}return instance;}}}
該實現主要有兩個優點:
但是,這種實現的主要缺點是在多線程環境下它是不安全的。如果執行過程的不同線程同時進入 Instance 屬性方法,那麼可能會建立多個 Singleton 對象執行個體。每個線程都會執行下列語句,並決定必須建立新的執行個體:
if (instance == null)
解決此問題的方法有很多。一種方法是使用被稱為 Double-Check Locking [Lea99] 的技術。而 C# 與公用語言運行庫也提供了一種"靜態初始化"方法,這種方法不需要開發人員顯式地編寫安全執行緒代碼,即可解決這些問題。
靜態初始化
One of the reasons Design Patterns [Gamma95] 避免使用靜態初始化的原因之一是,C++ 規範在靜態變數的初始化順序方面留下了一些多義性。幸運的是,.NET Framework 通過其變數初始化處理方法解決了這種多義性:
public sealed class Singleton{private static readonly Singleton instance = new Singleton();private Singleton(){}public static Singleton Instance{get{return instance;}}}
在此策略中,將在第一次引用類的任何成員時建立執行個體。公用語言運行庫負責處理變數初始化。該類標記為 sealed 以阻止發生派生,而派生可能會增加執行個體。有關將類標記為 sealed 的利與弊的討論,請參閱 [Sells03]。此外,變數標記為 readonly,這意味著只能在靜態初始化期間(此處顯示的樣本)或在類建構函式中分配變數。
該實現與前面的樣本類似,不同之處在於它依賴公用語言運行庫來初始設定變數。它仍然可以用來解決 Singleton 模式試圖解決的兩個基本問題:全域訪問和執行個體化控制。公用靜態屬性為訪問執行個體提供了一個全域訪問點。此外,由於建構函式是私人的,因此不能在類本身以外執行個體化 Singleton 類;因此,變數引用的是可以在系統中存在的唯一的執行個體。
由於 Singleton 執行個體被私人靜態成員變數引用,因此在類首次被對 Instance 屬性的調用所引用之前,不會發生執行個體化。因此,與 Design Patterns 形式的 Singleton 一樣,該解決方案實現了懶執行個體化屬性的一種形式。
這種方法唯一的潛在缺點是,您對執行個體化機制的控制權較少。在 Design Patterns 形式中,您能夠在執行個體化之前使用非預設的建構函式或執行其他任務。由於在此解決方案中由 .NET Framework 負責執行初始化,因此您沒有這些選項。在大多數情況下,靜態初始化是在 .NET 中實現 Singleton 的首選方法。
多線程 Singleton
靜態初始化適合於大多數情形。如果您的應用程式必須延遲執行個體化、在執行個體化之前使用非預設的建構函式或執行其他任務、並且工作在多線程環境中,那麼您需要另一種解決方案。但是,在一些情況下,您無法像在"靜態初始化"樣本中那樣依賴公用語言運行庫來確保線程的安全性。在這種情況下,必須使用特定的語言功能來確保在存在多線程的情況下僅建立一個對象執行個體。更常見的解決方案之一是使用 Double-Check Locking [Lea99] 技術來阻止不同的線程同時建立 singleton 的新執行個體。
注意:公用語言運行庫解決了在其他環境中常見的、與使用 Double-Check Locking 有關的問題。有關這些問題的詳細資料,請參閱馬里蘭大學電腦科學系網站中的"The 'Double-Checked Locking Is Broken' Declaration",網址為 http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html.
下面的實現僅允許一個線程在尚未建立 Singleton 執行個體的情況下進入關鍵地區(該地區由 lock 塊標識)。
using System;public sealed class Singleton{private static volatile Singleton instance;private static object syncRoot = new Object();private Singleton() {}public static Singleton Instance{get{if (instance == null){lock (syncRoot){if (instance == null)instance = new Singleton();}}return instance;}}}
此方法確保了僅在需要執行個體時才會建立僅一個執行個體。此外,變數被聲明為 volatile,以確保只有在執行個體變數分配完成後才能訪問執行個體變數。最後,此方法使用 syncRoot 執行個體來進行鎖定(而不是鎖定類型本身),以避免發生死結。
此 double-check locking 方法解決了線程並發問題,同時避免在每個 Instance 屬性方法的調用中都出現獨佔鎖定。它還允許您將執行個體化延遲到第一次訪問對象時發生。實際上,應用程式很少需要這種類型的實現。大多數情況下,靜態初始化方法已經夠用。
結果上下文
在 C# 中實現 Singleton 具有下列優缺點:
優點
缺點
如果您的多線程應用程式需要進行顯式初始化,那麼必須採取措施以避免線程問題。