c#中單例模式詳解

2023-11-02 09:00:19

基礎介紹:

 確保一個類只有一個範例,並提供一個全域性存取點。

  適用於需要頻繁範例化然後銷燬的物件,建立物件消耗資源過多,但又經常用到的物件,頻繁存取資料庫或檔案的物件。

  其本質就是保證在整個應用程式生命週期中,任何一個時刻,單例類的範例都只存在一個

  • 特性和功能:確保一個類只有一個範例,並提供一個全域性存取點。
  • 使用環境:當類只需要一個範例,且易於存取,且範例應在整個應用程式中共用時。
  • 注意事項:需要注意執行緒安全問題。
  • 優點:可以確保一個類只有一個範例,減少了記憶體開銷。
  • 缺點:沒有介面,擴充套件困難。  

應用場景:

  單例模式通常適用於在整個應用程式中只需要一個範例化物件的場景,以確保資源的高效利用和應用程式的穩定性。(共用資源)

  資源共用的情況下,避免由於資源操作時導致的效能或損耗等。

  控制資源的情況下,方便資源之間的互相通訊。如執行緒池等。

  • 紀錄檔系統:在應用程式中,通常只需要一個紀錄檔系統,以避免在多個地方建立多個紀錄檔物件。這一般是由於共用的紀錄檔檔案一直處於開啟狀態,所以只能有一個範例去操作,否則內容不好追加也有可能造成資源佔用加劇資源消耗。
  • 資料庫連線池:在應用程式中,資料庫連線池是一個非常重要的資源,單例模式可以確保在應用程式中只有一個資料庫連線池範例,避免資源浪費。主要是節省開啟或者關閉資料庫連線所引起的效率損耗,因為何用單例模式來維護,就可以大大降低這種損耗。
  • 組態檔管理器:在應用程式中,通常只需要一個組態檔管理器來管理應用程式的組態檔,單例模式可以確保在整個應用程式中只有一個組態檔管理器範例。這個是由於組態檔是共用的資源。
  • 快取系統:在應用程式中,快取系統是一個重要的元件,單例模式可以確保在整個應用程式中只有一個快取範例,以提高應用程式的效能。
  • 網站線上人數統計:其實就是全域性計數器,也就是說所有使用者在相同的時刻獲取到的線上人數數量都是一致的。
  • GUI元件:在圖形化使用者介面(GUI)開發中,單例模式可以確保在整個應用程式中只有一個GUI元件範例,以確保使用者介面的一致性和穩定性。

建立方式:

  餓漢式:類載入就會導致該單範例物件被建立。(靜態變數方式、靜態程式碼塊方式)

  懶漢式:類載入不會導致該單範例物件被建立,而是首次使用該物件時才會建立。(執行緒不安全型、執行緒安全型、雙重檢查鎖)

 

  1. 懶漢式---非執行緒安全型

     1 public class Singleton
     2     {
     3         //定義一個私有的靜態全域性變數來儲存該類的唯一範例
     4         private static Singleton singleton;
     5 
     6         /// <summary>
     7         /// 建構函式
     8         /// </summary>
     9         private Singleton()
    10         {
    11             //必須是私有的建構函式,這樣就可以保證該類無法通過new來建立該類的範例。
    12             //想要使用該類只能通過唯一存取點GetInstance()。
    13         }
    14 
    15         /// <summary>
    16         /// 全域性存取點
    17         /// 設定為靜態方法則可在外邊無需建立該類的範例就可呼叫該方法
    18         /// </summary>
    19         /// <returns></returns>
    20         public static Singleton GetInstance()
    21         {
    22             if (singleton == null)
    23             {
    24                 singleton = new Singleton();
    25             }
    26             return singleton;
    27         }
    28     }

    上面的程式碼中,由於建構函式被設定為 private 了,無法再在 Singleton 類的外部使用 new 來範例化一個範例,只能通過存取 GetInstance()來存取 Singleton 類。

    GetInstance()通過如下方式保證該 Singleton 只存在一個範例:

    首先這個 Singleton 類會在在第一次呼叫 GetInstance()時建立一個範例(第24行),並將這個範例的參照封裝在自身類中的靜態全域性變數singleton(第4行)

    然後以後呼叫 GetInstance()時就會判斷這個 Singleton 是否存在一個範例了(第22行),如果存在,則不會再建立範例。

    這樣就實現了懶載入的效果。但是,如果是多執行緒環境,會出現執行緒安全問題。

    比如多個執行緒同時執行GetInstance()方法時都走到了第22行,這個時候一個執行緒進入 if 判斷語句後但還沒有範例化 Singleton 時,第二個執行緒到達,此時 singleton 還是為 null

    如此會造成多個執行緒都會進入 if 執行程式碼塊中即都會執行第24行,這樣的話,就會建立多個範例違背了單裡模式,因此引出了範例2執行緒安全型。

     

  2. 懶漢式---執行緒安全型

     1 public class Singleton
     2     {
     3         //定義一個私有的靜態全域性變數來儲存該類的唯一範例
     4         private static Singleton singleton;
     5 
     6         //執行緒鎖
     7         private static readonly object _Object = new object();
     8 
     9         /// <summary>
    10         /// 建構函式
    11         /// </summary>
    12         private Singleton()
    13         {
    14             //必須是私有的建構函式,這樣就可以保證該類無法通過new來建立該類的範例。
    15             //想要使用該類只能通過唯一存取點GetInstance()。
    16         }
    17 
    18         /// <summary>
    19         /// 全域性存取點
    20         /// 設定為靜態方法則可在外邊無需建立該類的範例就可呼叫該方法
    21         /// </summary>
    22         /// <returns></returns>
    23         public static Singleton GetInstance()
    24         {
    25             lock (_Object)
    26             {
    27                 if (singleton == null)
    28                 {
    29                     singleton = new Singleton();
    30                 }
    31             }
    32             return singleton;
    33         }
    34     }

    相比範例1中可以看到在類中有定義了一個靜態的唯讀物件  _Object(第7行),該物件主要是提供給lock 關鍵字使用。

    lock關鍵字引數必須為基於參照型別的物件,該物件用來定義鎖的範圍。

    當多個執行緒同時進入GetInstance()方法時,由於存在鎖機制,當一個執行緒進入lock程式碼塊時,其餘執行緒會在lock語句的外部等待。

    當第一個執行緒執行完第29行建立物件範例後,便會退出鎖定區域,這個時候singleton變數已經不為null了。

    所以餘下執行緒再次進入lock程式碼塊時,由於第27行的原因則不會再次建立物件的範例。

    但這裡就涉及一個效能問題了,每一次有執行緒進入 GetInstance()時,均會執行鎖定操作來實現執行緒同步,這是非常耗費效能的。

    解決這個問題也很簡單,進行雙重檢查鎖定判斷即範例3。

     

  3. 懶漢式---雙重檢查鎖

     1 public class Singleton
     2     {
     3         //定義一個私有的靜態全域性變數來儲存該類的唯一範例
     4         private static Singleton singleton;
     5 
     6         //執行緒鎖
     7         private static readonly object _Object = new object();
     8 
     9         /// <summary>
    10         /// 建構函式
    11         /// </summary>
    12         private Singleton()
    13         {
    14             //必須是私有的建構函式,這樣就可以保證該類無法通過new來建立該類的範例。
    15             //想要使用該類只能通過唯一存取點GetInstance()。
    16         }
    17 
    18         /// <summary>
    19         /// 全域性存取點
    20         /// 設定為靜態方法則可在外邊無需建立該類的範例就可呼叫該方法
    21         /// </summary>
    22         /// <returns></returns>
    23         public static Singleton GetInstance()
    24         {
    25             if (singleton == null)//第一重
    26             {
    27                 lock (_Object)
    28                 {
    29                     if (singleton == null)//第二重
    30                     {
    31                         singleton = new Singleton();
    32                     }
    33                 }
    34             }
    35             return singleton;
    36         }
    37     }

    相比範例2來看,只是增加了第25行。

    在多執行緒中,當第一個執行緒建立完物件的範例後,singleton變數已經不為null了。之後再存取GetInstance()方法時,將不會再進行lock等待。

    如果沒有這行的情況下,每次多執行緒同時進入GetInstance()方法時,多餘的執行緒都會進入lock進行等待。這是非常耗費效能的。

    相比呼叫GetInstance()方法來作為全域性存取點還有另外一種寫法:

     1  public class Singleton
     2     {
     3         private static Singleton instance;
     4 
     5         private Singleton() { }
     6 
     7         public static Singleton Instance
     8         {
     9             get
    10             {
    11                 if (instance == null)
    12                 {
    13                     instance = new Singleton();
    14                 }
    15                 return instance;
    16             }
    17         }
    18     }

    前三個範例在使用者端呼叫:Singleton singletonOne = Singleton.GetInstance();

    後一種則可以直接:Singleton.Instance進行使用。

     

  4. 餓漢式

     1 public sealed class Singleton
     2     {
     3         //定義一個私有靜態的唯讀的全域性變數
     4         private static readonly Singleton singleton = new Singleton();
     5 
     6         /// <summary>
     7         /// 建構函式
     8         /// </summary>
     9         private Singleton()
    10         {
    11             //必須是私有的建構函式,這樣就可以保證該類無法通過new來建立該類的範例。
    12             //想要使用該類只能通過唯一存取點GetInstance()。
    13         }
    14 
    15         /// <summary>
    16         /// 全域性存取點
    17         /// 設定為靜態方法則可在外邊無需建立該類的範例就可呼叫該方法
    18         /// </summary>
    19         /// <returns></returns>
    20         public static Singleton GetInstance()
    21         {
    22             return singleton;
    23         }
    24     }

    在c#中使用靜態初始化時無需顯示地編寫執行緒安全程式碼,C# 與 CLR 會自動解決前面提到的懶漢式單例類時出現的多執行緒同步問題。

    當整個類被載入的時候,就會自行初始化 singleton 這個靜態唯讀變數。

    而非在第一次呼叫 GetInstance()時再來範例化單例類的唯一範例,所以這就是一種餓漢式的單例類。

總結:

  Singleton(單例):在單例類的內部實現只生成一個範例,同時它提供一個靜態的getInstance()工廠方法,讓客戶可以存取它的唯一範例;為了防止在外部對其範例化,將其建構函式設計為私有;在單例類內部定義了一個Singleton型別的靜態物件,作為外部共用的唯一範例。

  (1)資源共用的情況下,避免由於資源操作時導致的效能或損耗等。如紀錄檔檔案,應用設定。

  (2)控制資源的情況下,方便資源之間的互相通訊。如執行緒池等。