Head First設計模式——單例模式

  • 2019 年 11 月 11 日
  • 筆記

單例模式是所有設計模式中最簡單的模式,也是我們平常經常用到的,單例模式通常被我們應用於執行緒池、快取操作、隊列操作等等。

單例模式旨在創建一個類的實例,創建一個類的實例我們用全局靜態變數或者約定也能辦到單例的作用,為什麼我們要用單例模式?

接下來我們就從如何形成單例模式,單例模式創建的過程來講解。

1、單例如何形成

我們平常創建一個對象需要new對象,假如有一個對象ObjectClass我們實例化它。

new ObjectClass()

如果另外一個類要使用ObjectClass則可以再通過new來創建另外一個實例化,如果這個類是public 則我們可以在使用的時候多次實例化對象。

那我們怎麼保證類不被其他類實例化,利用private關鍵字我們可以採用私有構造函數來阻止外部實例化該類。

public class ObjectClass  {     private ObjectClass()      {      }  }  

這樣一來我們無法實例化ObjectClass則我們就無法使用它。那我們要怎麼實例化呢?

由於私有構造方法我們只能在內部訪問,所以我們可以用一個內部方法實例化ObjectClass,為了外部能夠訪問這個方法我們將這個方法設置成static。

這樣做了之後確保返回對象始終是第一次創建的對象,我們用一個私有靜態對象來存儲實例化的對象,如果對象沒創建我們則立即創建,如果已經創建就返回已經創建的對象。

    public class ObjectClass      {          private static ObjectClass singleton;          private ObjectClass()          {          }            public static ObjectClass GetSingletone()          {              if (singleton == null)              {                  singleton = new ObjectClass();              }              return singleton;          }      }

到這裡我們的單例模式就形成了,單例模式定義:

單例模式:確保一個類只有一個實例,並提供一個全局訪問點。

2、多執行緒導致單例模式問題

啟用多執行緒測試單例返回對象

    class Program      {          static void Main(string[] args)          {              for (int i = 0; i < 10; i++)              {                  TestSingleton();              }              Console.ReadKey();          }            public static void TestSingleton()          {              Task.Factory.StartNew(new Action(() =>              {                  var hc = ObjectClass.GetSingletone().GetHashCode();                  Console.WriteLine(hc);              }));          }      }  

  

如圖中做的測試一樣,我啟了10個執行緒獲得單例對象然後列印對象的HashCode。測試發現有HashCode不一致的情況,證明單例返回的對象並不是只有一個。

因為多執行緒運行的時候可能會同時進入if (singleton == null)的判斷,如果此時singleton變數還沒被實例化則可能有多個執行緒進入到實例化程式碼,以至於返回的實例化對象不是同一個。

3、解決多執行緒單例問題

由於多執行緒導致if檢查變數問題,則爭對檢查問題我們可以有兩類解決辦法:

①”急切”創建實例,不用延遲實例化做法

急切實例化就是在靜態初始化器中創建對象,這樣就保證了程式運行階段單例對象已經創建好,去除if判斷。

    public class ObjectClass      {          private static ObjectClass singleton=new ObjectClass();          private ObjectClass()          {          }            public static  ObjectClass GetSingletone()          {              return singleton;          }      }  

加鎖

為了讓創建對象只能有一個執行緒操作,則我們對創建對象程式碼進行加鎖處理,再次改造GetSingletone方法。

    public class ObjectClass      {          private static ObjectClass singleton = new ObjectClass();          private static object lockObj = new object();          private ObjectClass()          {          }            public static ObjectClass GetSingletone()          {              lock (lockObj)              {                  if (singleton == null)                  {                      singleton = new ObjectClass();                  }              }              return singleton;          }      }  

 加鎖對性能有一定的損耗,如果你的系統對性能要求比較高,我們對於加鎖的處理還有一種優化方式:雙重檢查加鎖

     public static ObjectClass GetSingletone()          {              if (singleton == null)              {                  lock (lockObj)                  {                      if (singleton == null)                      {                          singleton = new ObjectClass();                      }                  }              }              return singleton;          }  

使用雙重檢查加鎖,則多執行緒在運行的時候如果已經創建了單例對象後就不會再進入到lock程式碼段以此減少鎖帶來的性能損耗。

然後我們再來測試一波,啟用50個執行緒,可以看到輸出的HashCode是一致的。

4、總結

回到我們開始講的為什麼不用全局變數或者約定來解決單例問題,因為對於我們開發來說雖然有約定但是我們不能保證每個人都按照約定或者濫用全局變數造成問題。

而使用單例模式能進行更好的自我約定和管理,當然我們也有可能會濫用單例模式,這就需要對它能解決什麼問題如何使用深入理解。

設計模式並不是要生搬硬套,而是在需要的時候符合的場景進行合理使用。

雖然單例模式比較簡單,但通過分析我們看到問題也不少,要更好的使用需要我們更好的分析,也希望這篇博文對你有些幫助。