首頁  >  文章  >  後端開發  >  C#單例模式的實作以及效能比較的實例

C#單例模式的實作以及效能比較的實例

黄舟
黄舟原創
2018-05-16 17:08:521983瀏覽

這篇文章主要介紹了淺談C#單例模式的實現和性能對比的相關資料,詳細的介紹了6種實現方式,需要的朋友可以參考下

##簡介

單例指的是只能存在一個實例的類別(在C#中,更準確的說法是在每個AppDomain之中只能存在一個實例的類,它是軟體工程中使用最多的幾種模式之一。單例會在第一次被使用時建立。有很多種,但從最簡單的實現(非延遲加載,非線程安全,效率低下),到可延遲加載,線程安全,且高效的實現,它們都有一些基本的共同點:

單例類別都只有一個private的無參構造函數
  • 類別宣告為sealed(不是必須的)
  • 類別中有一個靜態變數保存所建立的實例的參考
  • 單例類別會提供一個靜態方法或屬性來傳回所建立的實例的參考(eg.GetInstance)
  • 幾種實作

一非執行緒安全性

//Bad code! Do not use!
public sealed class Singleton
{
  private static Singleton instance = null;
  private Singleton()
  {
  }

  public static Singleton instance
  {
    get
    {
      if (instance == null)
      {
        instance = new Singleton();
      }
      return instance;
    }
  }
}
這種方法不是執行緒安全的,會存在兩個執行緒同時執行if (instance == null)並且建立兩個不同的instance,後面建立的會替換掉新建立的,導致之前拿到的reference為空。 ##二簡單的線程安全實作

public sealed class Singleton
{
  private static Singleton instance = null;
  private static readonly object padlock = new object();

  Singleton()
  {
  }

  public static Singleton Instance
  {
    get
    {
      lock (padlock)
      {
        if (instance == null)
        {
          instance = new Singleton();
        }
        return instance;
      }
    }
  }
}

相比較於實作一,這個版本加上了一個對instance的鎖,在呼叫instance之前要先對padlock上鎖,這樣就避免了實現一中的執行緒衝突,該實作自始至終只會創建一個instance了。 ##注意這裡我們使用的是新建一個private的object實例padlock來實現鎖操作,而不是直接對Singleton進行上鎖。它會在任何code裡調用,直接對它上鎖會導致效能問題,甚至會出現死鎖情況。 ,但是不同執行緒之間如果同時上鎖,就可能會出現執行緒等待,或嚴重的會出現死鎖情況。因此,我們在使用lock時,盡量選擇類別中的私有變數上鎖,這樣可以避免上述情況發生。

三雙驗證的執行緒安全實作

public sealed calss Singleton
{
  private static Singleton instance = null;
  private static readonly object padlock = new object();

  Singleton()
  {
  }

  public static Singleton Instance
  {
    get
    {
      if (instance == null)
      {
        lock (padlock)
        {
          if (instance == null)
          {
            instance = new Singleton();
          }
        }
      }
      return instance;
    }
  } 
}

在保證執行緒安全的同時,這個實作也避免了每次呼叫Instance都進行lock操作,這會節約一定的時間。

但是,這種實作也有它的缺點:

1無法在Java中運作。 (具體原因可以見原文,這邊沒怎麼理解)

2程式設計師在自己實作時很容易出錯。如果對這個模式的程式碼進行自己的修改,要倍加小心,因為double check的邏輯較為複雜,很容易出現思考不周而出錯的情況。

四不用鎖的執行緒安全實作

public sealed class Singleton
{
  //在Singleton第一次被调用时会执行instance的初始化
  private static readonly Singleton instance = new Singleton();

  //Explicit static consturctor to tell C# compiler 
  //not to mark type as beforefieldinit
  static Singleton()
  {
  }

  private Singleton()
  {
  }

  public static Singleton Instance
  {
    get
    {
      return instance;
    }
  }
}

這個實作很簡單,並沒有用到鎖,但是它仍然是執行緒安全的。這裡使用了一個static,readonly的Singleton實例,它會在Singleton第一次被呼叫的時候新建一個instance,這裡新建時候的線程安全保障是由.NET直接控制的,我們可以認為它是一個原子操作,並且在一個AppDomaing中它只會被創建一次。

這種實作也有一些缺點:

1instance被創建的時機不明,任何對Singleton的呼叫都會事先建立instance2static建構子的循環調用。如有A,B兩個類,A的靜態建構子中調用了B,而B的靜態建構函數中又調用了A,這兩個就會形成一個循環調用,嚴重的會導致程式崩潰。 3我們需要手動加入Singleton的靜態建構子來確保Singleton型別不會被自動加上beforefieldinit這個Attribute,以此來確保instance會在第一次呼叫Singleton時才被建立。

4readonly的屬性無法在執行時改變,如果我們需要在程式執行時dispose這個instance再重新建立一個新的instance,這種實作方法就無法滿足。

五完全延遲載入實作(fully lazy instantiation)

public sealed class Singleton
{
  private Singleton()
  {
  }

  public static Singleton Instance 
  {
    get
    {
      return Nested.instance;
    }
  }

  private class Nested
  {
    // Explicit static constructor to tell C# compiler
    // not to mark type as beforefieldinit
    static Nested()
    {
    }

    internal static readonly Singleton instance = new Singleton();
  }
}

實現五是實現四的包裝。它確保了instance只會在Instance的get方法裡面調用,並且只會在第一次調用前初始化。它是實現四的確保延遲載入的版本。


六使用.NET4的Lazy8742468051c85b06f0a0af9e3e506b5c類型

public sealed class Singleton
{
  private static readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton());

  public static Singleton Instance 
  {
    get 
    {
      return lazy.Value;
    }
  }

  private Singleton()
  {
  }
}

.NET4或以上的版本支援Lazy8742468051c85b06f0a0af9e3e506b5c來實現延遲加載,它用最簡潔的程式碼保證了單例的線程安全和延遲載入特性。

效能差異

之前的實作中,我們都在強調程式碼的執行緒安全性和延遲載入。然而在實際使用中,如果你的單例類別的初始化不是一個很耗時的操作或初始化順序不會導致bug,延遲初始化是一個可有可無的特性,因為初始化所佔用的時間是可以忽略不計的。

在實際使用場景中,如果你的單例實例會被頻繁地呼叫(如在一個循環中),那麼為了確保執行緒安全而帶來的效能消耗是更值得關注的地方。

為了比較這幾種實現的效能,我做了一個小測試,循環拿這些實作中的單例9億次,每次呼叫instance的方法執行一個count++操作,每隔一百萬輸出一次,運作環境是MBP上的Visual Studio for Mac。結果如下:


執行緒安全性 延遲載入 測試運行時間(ms)
實作一 #是 15532
實作二 45803
實作三 15953
實作四 不完全 14572
實作五 #14295
實作六 22875

測試方法並不嚴謹,但仍然可以看出,方法二由於每次都需要呼叫lock,是最耗時的,幾乎是其他幾個的三倍。排第二的則是使用.NET Lazy類型的實現,比其他多了二分之一左右。其餘的四個,則沒有明顯差異。

總結

整體來說,上面說的多種單例實作方式在現今的電腦效能下差距都不大,除非你需要特別大並發量的呼叫instance,才會需要去考慮鎖定的效能問題。

對於一般的開發者來說,使用方法二或方法六來實現單例已經是足夠好的了,方法四和五則需要對C#運行流程有一個較好的認識,並且實現時需要掌握一定技巧,並且他們節省的時間仍然是有限的。

引用

本文大部分是翻譯自Implementing the Singleton Pattern in C#,加上了一部分自己的理解。這是我搜尋static readonly field initializer vs static constructor initialization時看到的,在這裡對兩位作者表示感謝。

以上是C#單例模式的實作以及效能比較的實例的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn