設計模式之單例模式

2023-01-11 09:00:41

本文已經收錄到Github倉庫,該倉庫包含計算機基礎、Java基礎、多執行緒、JVM、資料庫、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分散式、微服務、設計模式、架構、校招社招分享等核心知識點,歡迎star~

Github地址:https://github.com/Tyson0314/Java-learning

單例模式

單例模式(Singleton),目的是為了保證在一個程序中,某個類有且僅有一個範例。

由於這個類只有一個範例,所以不能讓呼叫方使用new Xxx()來建立範例。所以,單例的構造方法必須是private,這樣就防止了呼叫方自己建立範例。

單例模式的實現需要三個必要的條件

  1. 單例類的建構函式必須是私有的,這樣才能將類的建立權控制在類的內部,從而使得類的外部不能建立類的範例。
  2. 單例類通過一個私有的靜態變數來儲存其唯一範例。
  3. 單例類通過提供一個公開的靜態方法,使得外部使用者可以存取類的唯一範例。

另外,實現單例類時,還需要考慮三個問題:

  • 建立單例物件時,是否執行緒安全
  • 單例物件的建立,是否延時載入
  • 獲取單例物件時,是否需要加鎖

下面介紹幾種實現單例模式的方式。

餓漢模式

JVM在類的初始化階段,會執行類的靜態方法。在執行類的初始化期間,JVM會去獲取Class物件的鎖。這個鎖可以同步多個執行緒對同一個類的初始化。

餓漢模式只在類載入的時候建立一次範例,沒有多執行緒同步的問題。單例沒有用到也會被建立,而且在類載入之後就被建立,記憶體就被浪費了。

public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton() {}  
    public static Singleton newInstance() {
        return instance;  
    }  
}

餓漢式單例的優點

  • 單例物件的建立是執行緒安全的;
  • 獲取單例物件時不需要加鎖

餓漢式單例的缺點:單例物件的建立,不是延時載入

懶漢式

與餓漢式思想不同,懶漢式支援延時載入,將物件的建立延遲到了獲取物件的時候。不過為了執行緒安全,在獲取物件的操作需要加鎖,這就導致了低效能。

public class Singleton { 
  private static final Singleton instance;
  
  private Singleton () {}
  
  public static synchronized Singleton getInstance() {    
    if (instance == null) {      
      instance = new Singleton();    
    }    

    return instance;  
  }
}

上述程式碼加的鎖只有在第一次建立物件時有用,而之後每次獲取物件,其實是不需要加鎖的(雙重檢查鎖定優化了這個問題)。

懶漢式單例優點

  • 物件的建立是執行緒安全的。
  • 支援延時載入。

懶漢式單例缺點

  • 獲取物件的操作被加上了鎖,影響了並行效能。

雙重檢查鎖定

雙重檢查鎖定將懶漢式中的 synchronized 方法改成了 synchronized 程式碼塊。如下:

public class Singleton {  
    private static volatile Singleton instance = null;  //volatile
    private Singleton(){}  
    public static Singleton getInstance() {  
        if (instance == null) {  
            synchronized (Singleton.class) {  
                if (instance == null) {
                    instance = new Singleton();  
                }  
            }  
        }  
        return instance;  
    }  
}  

雙重校驗鎖先判斷 instance 是否已經被範例化,如果沒有被範例化,那麼才對範例化語句進行加鎖。

instance使用static修飾的原因:getInstance為靜態方法,因為靜態方法的內部不能直接使用非靜態變數,只有靜態成員才能在沒有建立物件時進行初始化,所以返回的這個範例必須是靜態的。

為什麼兩次判斷instance == null

Time Thread A Thread B
T1 檢查到instance為空
T2 檢查到instance為空
T3 初始化物件A
T4 返回物件A
T5 初始化物件B
T6 返回物件B

new Singleton()會執行三個動作:分配記憶體空間、初始化物件和物件參照指向記憶體地址。

memory = allocate();  // 1:分配物件的記憶體空間
ctorInstance(memory);  // 2:初始化物件
instance = memory;    // 3:設定instance指向剛分配的記憶體地址

由於指令重排優化的存在,導致初始化物件和將物件參照指向記憶體地址的順序是不確定的。在某個執行緒建立單例物件時,會為該物件分配了記憶體空間並將物件的欄位設定為預設值。此時就可以將分配的記憶體地址賦值給instance欄位了,然而該物件可能還沒有初始化。若緊接著另外一個執行緒來呼叫getInstance,取到的是未初始化的物件,程式就會出錯。volatile 可以禁止指令重排序,保證了先初始化物件再賦值給instance變數。

Time Thread A Thread B
T1 檢查到instance為空
T2 獲取鎖
T3 再次檢查到instance為空
T4 instance分配記憶體空間
T5 instance指向記憶體空間
T6 檢查到instance不為空
T7 存取instance(此時物件還未完成初始化)
T8 初始化instance

雙重檢查鎖定單例優點

  • 物件的建立是執行緒安全的。
  • 支援延時載入。
  • 獲取物件時不需要加鎖。

靜態內部類

它與餓漢模式一樣,也是利用了類初始化機制,因此不存在多執行緒並行的問題。不一樣的是,它是在內部類裡面去建立物件範例。這樣的話,只要應用中不使用內部類,JVM就不會去載入這個單例類,也就不會建立單例物件,從而實現懶漢式的延遲載入。也就是說這種方式可以同時保證延遲載入和執行緒安全。

基於類初始化的方案的實現程式碼更簡潔。

public class Instance {
    private static class InstanceHolder {
        public static Instance instance = new Instance();
    }
    private Instance() {}
    public static Instance getInstance() {
        return InstanceHolder.instance ;  // 這裡將導致InstanceHolder類被初始化
    }
}

如上述程式碼,InstanceHolder 是一個靜態內部類,當外部類 Instance 被載入的時候,並不會建立 InstanceHolder 範例物件。

只有當呼叫 getInstance() 方法時,InstanceHolder 才會被載入,這個時候才會建立 InstanceInstance 的唯一性、建立過程的執行緒安全性,都由 JVM 來保證。

靜態內部類單例優點

  • 物件的建立是執行緒安全的。
  • 支援延時載入。
  • 獲取物件時不需要加鎖。

列舉

用列舉來實現單例,是最簡單的方式。這種實現方式通過 Java 列舉型別本身的特性,保證了範例建立的執行緒安全性和範例的唯一性。

public enum Singleton {
  INSTANCE; // 該物件全域性唯一
}

最後給大家分享一個Github倉庫,上面有大彬整理的300多本經典的計算機書籍PDF,包括C語言、C++、Java、Python、前端、資料庫、作業系統、計算機網路、資料結構和演演算法、機器學習、程式設計人生等,可以star一下,下次找書直接在上面搜尋,倉庫持續更新中~

Github地址https://github.com/Tyson0314/java-books