第5章分佈式系統模式 Singleton

上下文

在某些情況下,特定類型的數據需要提供給應用程序中的其他所有對象使用。在大多數情況下,這種類型的數據在系統中還是唯一的。例如,用戶界面只能有一個所有應用程序必須訪問的鼠標指針。同樣,企業解決方案可能用單網關對象作爲接口來管理與特定舊系統的連接。

影響因素

以下因素影響這種情況中的系統,在考慮上述問題的解決方案時必須協調這些影響因素:

  • 很多編程語言(例如 Microsoft Visual Basic® 6.0 版或 C++)都支持全局對象的定義。這些對象位於命名空間的根部,應用程序中所有對象都可全局使用這些對象。此方法爲全局可訪問性問題提供了一個簡單的解決方 案,但是不能解決單實例要求。它無法禁止其他對象創建全局對象的其他實例。另外,其他面向對象的語言(例如 Visual Basic .NET 或 C#)不直接支持全局變量。

  • 要確保某類僅有一個實例,則必須控制實例化過程。因此需要通過使用編程語言中的固有實例化機制(例如通過使用 new 運算符)來阻止其他對象創建該類的實例。控制實例化的另一方面是提供集中機制,所有對象都可通過該機制獲得對此單個實例的引用。

解決方案

Singleton 通過下列方法提供唯一的全局實例:

  • 讓類創建自己的唯一實例。

  • 允許其他對象通過可返回實例引用的類方法來訪問此實例。類方法是全局可訪問的。

  • 將類構造函數聲明爲私有,從而任何其他對象都不能創建新實例。

圖 1 顯示了該模式的靜態結構。UML 類圖表非常簡單,這是因爲 Singleton 由一個簡單類組成,而該類包含了對其自身的單個實例的引用。

 

圖 1:Singleton 結構

圖 1 顯示,Singleton 類包含了公用類作用範圍(靜態)屬性,該屬性會返回對 Singleton 類的單個實例的引用。(UML 中的下劃線表示類作用範圍屬性。)另外,右上角的數字 1 表示任何時候在系統中該類只能有一個實例。因爲 Singleton 的默認構造函數是私有的,因此係統中的任何其他對象都必須通過 Instance 屬性才能訪問 Singleton 對象。

通常將 Singleton 模式歸類爲慣用語,而不是模式,這是因爲該解決方案主要取決於所用編程語言的功能(例如類方法和靜態初始值)。正如該模式集合所做的那樣,將抽象的概念與特定的實現分隔開來可能會使 Singleton 實現看起來非常簡單。

結果上下文

Singleton 會導致下列優缺點:

優點

  • 實例控制 Singleton 會阻止其他對象實例化其自己的 Singleton 對象的副本,從而確保所有對象都訪問唯一實例。

  • 靈活性。因爲類控制了實例化過程,所以類可以靈活更改實例化過程。

缺點

  • 開銷。雖然數量很少,但如果每次對象請求引用時都要檢查是否存在類的實例,將仍然需要一些開銷。可以通過使用靜態初始化解決此問題,相關敘述請參閱"在 C# 中實現 Singleton"。

  • 可能的開發混淆。使用 singleton 對象(尤其在類庫中定義的對象)時,開發人員必須記住自己不能使用 new 關鍵字實例化對象。因爲可能無法訪問庫源代碼,因此應用程序開發人員可能會意外發現自己無法直接實例化此類。

  • 對象生存期。 Singleton 不能解決刪除單個對象的問題。在提供內存管理的語言中(例如基於 .NET Framework 的語言),只有 Singleton 類能夠導致實例被取消分配,因爲它包含對該實例的私有引用。在某些語言中(如 C++),其他類可以刪除對象實例,但這樣會導致 Singleton 類中出現懸浮引用。

在 C# 中實現 Singleton

上下文

要在 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

但是,這種實現的主要缺點是在多線程環境下它是不安全的。如果執行過程的不同線程同時進入 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 的新實例。

下面的實現僅允許一個線程在尚未創建 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 具有下列優缺點:

優點

  • 由於 .NET Framework 顯式地指定靜態變量初始化如何以及何時發生,因此靜態初始化方法是可能的。

  • 列的前面的"多線程 Singleton"中所描述的 Double-Check Locking 技術已在公共語言運行庫中正確實現。

缺點

如果您的多線程應用程序需要進行顯式初始化,那麼必須採取措施以避免線程問題。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章