Java設計模式學習筆記(五) 單例模式

主宰命運者聯盟盟主發表於2019-07-18

前言

本篇是設計模式學習筆記的其中一篇文章,如對其他模式有興趣,可從該地址查詢設計模式學習筆記彙總地址

1. 使用單例模式的原因

以Windows工作管理員為例,在Windows系統中,工作管理員是唯一的,多次開啟工作管理員,始終只能彈出一個唯一的工作管理員.

這麼做的理由有兩個:

  1. 節約資源
  2. 避免多個例項資料不一致問題

1.1 節約資源

如果能彈出多個視窗,且這些視窗的內容完全一致,全部是重複物件,這勢必會浪費系統資源,工作管理員需要獲取系統執行時的諸多資訊,這些資訊的獲取需要消耗一定的系統資源,包括CPU資源及記憶體資源等,浪費是可恥的,而且根本沒有必要顯示多個內容完全相同的視窗

1.2 避免多個例項資料不一致問題

如果彈出的多個視窗內容不一致,問題就更加嚴重了,這意味著在某一瞬間系統資源使用情況和程式、服務等資訊存在多個狀態,例如工作管理員視窗A顯示“CPU使用率”為10%,視窗B顯示“CPU使用率”為15%,到底哪個才是真實的呢?這純屬“調戲”使用者,給使用者帶來誤解,更不可取.

為了確保物件的唯一性,我們可以通過單例模式來實現,這就是單例模式的動機所在

2. 單例模式概述

通過模擬Windows工作管理員,建立TaskManager類來理解單例模式

2.1 TaskManager

      /**
       * @author liuboren
       * @Title: 工作管理員類
       * @Description:
       * @date 2019/7/16 15:17
       */
      public class TaskManager {
          //初始化視窗
          public TaskManager() {
      
          }
      
          //顯示程式
          public void displayProcesses() {
      
          }
      
          //顯示服務
          public void displayServices() {
      
          }
      
      }
      

2.2 重構TaskManager

為了實現Windows工作管理員的唯一性,我們通過如下三步來對該類進行重構:

  • 建構函式使用private修飾
  • 定義一個TaskManager型別的私有成員變數
  • 增加共有靜態方法例項化TaskManager

2.2.1 建構函式使用private修飾

由於每次使用new關鍵字來例項化TaskManager類時都將產生一個新物件,為了確保TaskManager例項的唯一性,我們需要禁止類的外部直接使用new來建立物件,因此需要將TaskManager的建構函式的可見性改為private,如下程式碼所示:

          private TaskManager() {……}

2.2.2 定義一個TaskManager型別的私有成員變數

將建構函式改為private修飾後該如何建立物件呢?不要著急,雖然類的外部無法再使用new來建立物件,但是在TaskManager的內部還是可以建立的,可見性只對類外有效。因此,我們可以在TaskManager中建立並儲存這個唯一例項。為了讓外界可以訪問這個唯一例項,需要在TaskManager中定義一個靜態的TaskManager型別的私有成員變數,如下程式碼所示:

          private static TaskManager tm = null;

2.2.3 增加共有靜態方法例項化TaskManager

為了保證成員變數的封裝性,我們將TaskManager型別的tm物件的可見性設定為private,但外界該如何使用該成員變數並何時例項化該成員變數呢?答案是增加一個公有的靜態方法,
如下程式碼所示:

          public static TaskManager getInstance()
          {
          if (tm == null)
          {
          tm = new TaskManager();
          }
          return tm;
          }

2.2.4 程式碼

在類外我們無法直接建立新的TaskManager物件,但可以通過程式碼TaskManager.getInstance()來訪問例項物件,第一次呼叫getInstance()方法時將建立唯一例項,再次呼叫時將返回第一次建立的例項,從而確保例項物件的唯一性

          
          /**
           * @author liuboren
           * @Title: 單例版工作管理員類
           * @Description:
           * @date 2019/7/16 15:24
           */
          public class TaskManagerSingleton {
              private static TaskManagerSingleton taskManagerSingleton = null;
              
              //初始化視窗
              private TaskManagerSingleton() {
              }
          
              public static TaskManagerSingleton getInstance(){
                  if (taskManagerSingleton == null){
                      taskManagerSingleton = new TaskManagerSingleton();
                  }
                  return taskManagerSingleton;
              }
          
              //顯示程式
              public void displayProcesses() {
          
              }
          
              //顯示服務
              public void displayServices() {
          
              }
          }
          

2.3 定義

單例模式(Singleton Pattern):確保某一個類只有一個例項,而且自行例項化並向整個系統提供這個例項,這個類稱為單例類,它提供全域性訪問的方法。

單例模式是一種物件建立型模式。

2.4 三要點

1.某個類只能有一個例項.
2.它必須自行建立這個例項.
3.是它必須自行向整個系統提供這個例項.

2.5 結構圖

Java設計模式學習筆記(五) 單例模式

2.6 角色

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

3. 餓漢式單例類和懶漢式單例類

   public static TaskManagerSingleton getInstance(){
          if (taskManagerSingleton == null){
              taskManagerSingleton = new TaskManagerSingleton();
          }
          return taskManagerSingleton;
      }

在高併發環境下,以上程式碼是非執行緒安全的,可能在同一時刻,有兩個執行緒通過 if (taskManagerSingleton == null)的判斷,導致會例項化兩次物件.

要想解決這個問題,有兩種方式一種是懶漢式單例類.另一種是餓漢式單例類.

注: 如果對併發沒有過了解,可以看我之前的部落格併發程式設計學習筆記系列

3.1 餓漢式單例類

Java設計模式學習筆記(五) 單例模式

      /**
       * @author liuboren
       * @Title: 餓漢式單例
       * @Description:
       * @date 2019/7/16 16:30
       */
      public class EagerSingleton {
          private static final EagerSingleton instance = new EagerSingleton();
          private EagerSingleton() { }
          public static EagerSingleton getInstance() {
              return instance;
          }
      }

當類被載入時,靜態變數instance會被初始化,此時類的私有建構函式會被呼叫,單例類的唯一例項將被建立。餓漢式單例類可確保單例物件的唯一性。

3.2 懶漢式單例類

Java設計模式學習筆記(五) 單例模式

      /**
       * @author liuboren
       * @Title: 懶漢式單例類
       * @Description:
       * @date 2019/7/16 16:41
       */
      public class LazySingleton {
          private static LazySingleton instance = null;
      
          private LazySingleton() { }
          
          synchronized public static LazySingleton getInstance() {
              if (instance == null) {
                  instance = new LazySingleton();
              }
              return instance;
          }
      }
      

3.2.1 餓漢式單例類效能優化

該懶漢式單例類在getInstance()方法前面增加了關鍵字synchronized進行執行緒鎖,以處理多個執行緒同時訪問的問題。但是,上述程式碼雖然解決了執行緒安全問題,但是每次呼叫getInstance()時都需要進行執行緒鎖定判斷,在多執行緒高併發訪問環境中,將會導致系統效能大大降低。如何既解決執行緒安全問題又不影響系統效能呢?我們繼續對懶漢式單例進行改進。事實上,我們無須對整個getInstance()方法進行鎖定,只需對其中的程式碼“instance = new LazySingleton();”進行鎖定即可。因此getInstance()方法可以進行如下改進:

          
          public static LazySingleton getInstance() {
          
          if (instance == null) {
          
          synchronized (LazySingleton.class) {
          
          instance = new LazySingleton();
          
          }
          
          }
          
          return instance;
          
          }
          

3.2.2 使用雙重檢查鎖實現餓漢式單例類

問題貌似得以解決,事實並非如此。如果使用以上程式碼來實現單例,還是會存在單例物件不唯一。

原因如下:假如在某一瞬間執行緒A和執行緒B都在呼叫getInstance()方法,此時instance物件為null值,均能通過instance == null的判斷。由於實現了synchronized加鎖機制,執行緒A進入synchronized鎖定的程式碼中執行例項建立程式碼,執行緒B處於排隊等待狀態,必須等待執行緒A執行完畢後才可以進入synchronized鎖定程式碼。但當A執行完畢時,執行緒B並不知道例項已經建立,將繼續建立新的例項,導致產生多個單例物件,違背單例模式的設計思想,因此需要進行進一步改進,在synchronized中再進行一次(instance == null)判斷,這種方式稱為雙重檢查鎖定(Double-Check Locking)。使用雙重檢查鎖定實現的懶漢式單例類完整程式碼如下所示:

          
          /**
           * @author liuboren
           * @Title: 完美版 懶載入單例類
           * @Description:
           * @date 2019/7/16 16:47
           */
          public class LazySingletonPerfect {
              private volatile static LazySingletonPerfect instance = null;
          
              private LazySingletonPerfect() {
              }
          
              public static LazySingletonPerfect getInstance() {
                      //第一重判斷
                  if (instance == null) {
                      //鎖定程式碼塊
                      synchronized (LazySingleton.class) {
                      //第二重判斷
                          if (instance == null) {
                              instance = new LazySingletonPerfect(); //建立單例例項
                          }
                      }
                  }
                  return instance;
              }
          
          }
          

3.3 餓漢式單例類與懶漢式單例類比較

3.3.1 餓漢式

優點:

  1. 執行緒安全: 餓漢式單例類在類被載入時就將自己例項化,它的優點在於無須考慮多執行緒訪問問題,可以確保例項的唯一性

  2. 呼叫速度優於懶漢式: 從呼叫速度和反應時間角度來講,由於單例物件一開始就得以建立,因此要優於懶漢式單例

缺點: 資源利用效率不如懶漢式且載入時間較長無論系統在執行時是否需要使用該單例物件,由於在類載入時該物件就需要建立,因此從資源利用效率角度來講,餓漢式單例不及懶漢式單例,而且在系統載入時由於需要建立餓漢式單例物件,載入時間可能會比較長

3.3.2 懶漢式

優點:

  1. 延遲載入: 懶漢式單例類在第一次使用時建立,無須一直佔用系統資源,實現了延遲載入

缺點:

  1. 非執行緒安全: 多執行緒訪問懶漢式單例類可能會建立多個例項

  2. 效能稍差通過雙重檢查鎖定等機制保證執行緒安全,這將導致系統效能受到一定影響

4.更好的單例實現方法

餓漢式單例類不能實現延遲載入,不管將來用不用始終佔據記憶體;懶漢式單例類執行緒安全控制煩瑣,而且效能受影響。

可見,無論是餓漢式單例還是懶漢式單例都存在這樣那樣的問題,有沒有一種方法,能夠將兩種單例的缺點都克服,而將兩者的優點合二為一呢?答案是:Yes!下面我們來學習這種更好的被稱之為Initialization Demand Holder (IoDH)的技術

  /**
   * @author liuboren
   * @Title: IoDh技術
   * @Description: Java最好的單例實現模式
   * @date 2019/7/17 10:25
   */
  public class IoDHSingleton {
  
      private IoDHSingleton() {
      }
  
      //靜態內部類的成員變數才能是靜態的
      private static class HolderClas{
  
          public static IoDHSingleton ioDHSingleton = new IoDHSingleton();
      }
  
      public static  IoDHSingleton getInstance(){
          return HolderClas.ioDHSingleton;
      }
  
      public static void main(String[] args) {
          IoDHSingleton s1,s2;
          s1 = IoDHSingleton.getInstance();
          s2 = IoDHSingleton.getInstance();
          System.out.println(s1 == s2);
  
      }
  }
  

編譯並執行上述程式碼,執行結果為:true,即建立的單例物件s1和s2為同一物件。由於靜態單例物件沒有作為Singleton的成員變數直接例項化,因此類載入時不會例項化Singleton,第一次呼叫getInstance()時將載入內部類HolderClass,在該內部類中定義了一個static型別的變數instance,此時會首先初始化這個成員變數,由Java虛擬機器來保證其執行緒安全性,確保該成員變數只能初始化一次。由於getInstance()方法沒有任何執行緒鎖定,因此其效能不會造成任何影響。

通過使用IoDH,我們既可以實現延遲載入,又可以保證執行緒安全,不影響系統效能,不失為一種最好的Java語言單例模式實現方式(其缺點是與程式語言本身的特性相關,很多物件導向語言不支援IoDH)

5. 總結

單例模式作為一種目標明確、結構簡單、理解容易的設計模式,在軟體開發中使用頻率相當高,在很多應用軟體和框架中都得以廣泛應用

5.1 優點

(1) 單例模式提供了對唯一例項的受控訪問。因為單例類封裝了它的唯一例項,所以它可以嚴格控制客戶怎樣以及何時訪問它。

(2) 由於在系統記憶體中只存在一個物件,因此可以節約系統資源,對於一些需要頻繁建立和銷燬的物件單例模式無疑可以提高系統的效能。

(3) 允許可變數目的例項。基於單例模式我們可以進行擴充套件,使用與單例控制相似的方法來獲得指定個數的物件例項,既節省系統資源,又解決了單例單例物件共享過多有損效能的問題。

5.2 缺點

(1) 由於單例模式中沒有抽象層,因此單例類的擴充套件有很大的困難。

(2) 單例類的職責過重,在一定程度上違背了“單一職責原則”。因為單例類既充當了工廠角色,提供了工廠方法,同時又充當了產品角色,包含一些業務方法,將產品的建立和產品的本身的功能融合到一起。

(3) 現在很多物件導向語言(如Java、C#)的執行環境都提供了自動垃圾回收的技術,因此,如果例項化的共享物件長時間不被利用,系統會認為它是垃圾,會自動銷燬並回收資源,下次利用時又將重新例項化,這將導致共享的單例物件狀態的丟失

5.3 適用場景

(1) 系統只需要一個例項物件,如系統要求提供一個唯一的序列號生成器或資源管理器,或者需要考慮資源消耗太大而只允許建立一個物件。

(2) 客戶呼叫類的單個例項只允許使用一個公共訪問點,除了該公共訪問點,不能通過其他途徑訪問該例項。

相關文章