如何理解單例模式?

2023-04-08 12:00:50

單例模式(Singleton Pattern):採取一定的方法保證在整個的軟體系統中,對某個類只能存在一個物件範例,並且該類只提供一個取得其物件範例的方法。

通俗點來講:就是一個男人只能有一個老婆,一個女人只能有一個老公

單例模式一共有8種方式實現,下面一一舉例:

1、餓漢式(靜態常數屬性)

實現步驟

  1. 構造器私有化
  2. 在類的內部定義靜態常數物件
  3. 向外暴露一個靜態的公共方法getInstance,返回一個類的物件範例

參考程式碼

/**
 * Husband,一個女人只能有一個老公husband -> Husband實現單例模式
 * 方式1:餓漢式(靜態常數屬性)實現單例模式
 */
public class Husband {
    // 第二步:建立私有的靜態常數物件
    private static final Husband husband = new Husband();

    // 第一步:構造器私有化
    private Husband() {
    }

    // 第三步:向外提供一個靜態的公共方法,以獲取該類的物件
    public static Husband getInstance() {
        // 返回的永遠是 同一個 husband物件
        return husband;
    }
}

細節說明

  1. 為什麼構造器要私有化? -> 使外部無法建立Husband物件,只能使用已經準備好的物件 -> 不能濫情
  2. 為什麼屬性設定成final? -> 只建立這一個物件husband,以後不會再變 -> 一生只鍾情一人
  3. 為什麼設定成類變數static? -> 構造器已被私有化,外部無法建立Husband物件,需要類內部提前準備好物件 -> 在類載入時將物件準備好
  4. 為什麼公共方法要用static? -> 非static方法屬於物件,需要通過物件.方法名()呼叫 -> 外部類無法建立物件,在沒有物件時,外部類無法存取非static方法
  5. 為什麼叫餓漢式? -> 只要載入了類資訊,物件就已經建立好 -> 只要餓了,就吃東西

優缺點

  • 優點:實現了在整個軟體流程中建立一次類的物件,不存線上程安全問題(反射會破壞單例模式安全性),呼叫效率高
  • 缺點:如果只載入了類,並不需要用到該類物件,物件也已經建立好 -> 存在記憶體資源浪費問題。

2、餓漢式(靜態程式碼塊)

實現步驟

  1. 構造器私有化
  2. 定義靜態常數不初始化
  3. 靜態程式碼塊中為類屬性初始化建立物件
  4. 向外提供一個靜態的公共方法,以獲取該類的物件

參考程式碼

/**
 * Husband,一個女人只能有一個老公 -> Husband實現單例模式
 * 方式2:餓漢式(靜態程式碼塊)實現單例模式
 */
public class Husband {
    // 第二步:建立私有的靜態常數物件
    private static final Husband husband;

    static {
        husband = new Husband();
    }

    // 第一步:構造器私有化 -> 外部無法建立該類物件,只能使用已經準備好的物件
    private Husband() {
    }

    // 第三步:向外提供一個靜態的公共方法,以獲取該類的物件
    public static Husband getInstance() {
        // 返回的永遠是 同一個 husband物件
        return husband;
    }
}

細節說明:原理和第一種餓漢式相同,都是利用類載入時完成物件屬性的建立

優缺點同上一種餓漢式

3、懶漢式(執行緒不安全)

實現步驟

  1. 構造器私有化
  2. 定義一個私有靜態屬性物件
  3. 提供一個公共的static方法,可以返回一個類的物件

參考程式碼

/**
 * 一個男人只能有一個老婆wife -> Wife類實現單例模式
 * 方式3:懶漢式單例模式(執行緒不安全)
 */
public class Wife {
    // 第二步:設定私有靜態屬性
    private static Wife wife = null;

    // 第一步:構造器私有化
    private Wife() {
    }

    // 第三步:向外提供獲取物件的靜態方法
    public static Wife getInstance() {
        if (wife == null) { // 如果沒有老婆,則分配一個老婆
            wife = new Wife();
        }
        return wife;
    }
}

細節說明

  1. 為什麼叫懶漢式? -> 即使載入了類資訊,不呼叫getInstance()方法也不會建立物件 -> 餓了(載入類資訊)也不吃東西(不建立物件),懶到一定程度。
  2. 為什麼要if判斷? -> 防止每次呼叫時都會重新建立新的物件 -> 防止濫情

優缺點

  • 優點:只有需要物件時才會呼叫方法返回該類物件,沒有則建立物件,避免了資源浪費 -> 實現了延時載入

  • 缺點:執行緒不安全,分析if程式碼塊:

    if (wife == null) {
        // 當執行緒1進入if程式碼塊後,還沒有完成物件的建立之前,執行緒2緊隨其後也進入了if程式碼塊內
        // 此時就會出現執行緒1建立了物件,執行緒2也建立了物件 -> 破壞了單例模式 -> 執行緒不安全
        wife = new Wife();
        // 通俗的來講:多個女生看上了同一個男生,問男生有沒有老婆?男生回答沒有老婆 -> 多個女生先後都當過男生的老婆 -> 前妻太多 -> 違背了單例模式的"一生只鍾情一人"的核心思想
    }
    

4、懶漢式(同步程式碼塊)

參考程式碼

/**
 * 一個男人只能有一個老婆wife -> Wife類實現單例模式
 * 方式4:懶漢式單例模式(同步程式碼塊,執行緒安全,效能差)
 */
public class Wife {
    // 第二步:設定私有靜態屬性
    private static Wife wife = null;

    // 第一步:構造器私有化
    private Wife() {
    }

    // 第三步:向外提供獲取物件的靜態方法
    public static Wife getInstance() {
        synchronized(Wife.class) { // 同步程式碼塊,每個執行緒進入if判斷前都需要獲得互斥鎖,保證同一時間只有一個執行緒進入
            if (wife == null) {
                wife = new Wife();
            }
        }
        return wife;
    }
}

說明:給if語句加上synchronized關鍵字,保證每一次只有一個執行緒獲得互斥鎖進入同步程式碼塊,並且將同步程式碼塊全部執行完之後釋放鎖,切換其他執行緒執行,類似於資料庫中事務的概念(給SQL語句增加原子性),這裡是給if語句增加原子性,要麼全部執行,要麼都不執行。

優缺點

  • 優點:執行緒安全(反射會破壞安全性)

  • 缺點:效能差 -> 只有第一次建立物件時需要同步程式碼,確保同一時間只有一個執行緒進入if語句,後面執行緒再呼叫該方法時,物件已經建立好只需要直接返回 -> 每一次執行緒呼叫該方法後,都需要等待獲取其他執行緒釋放的互斥鎖 -> 浪費了大量時間在 等待獲取互斥鎖 上 -> 效率低下

    通俗的來講:多個女生問同一個男生有沒有老婆? -> 男生回答:需要成為物件才有資格知道(設定同步程式碼塊,執行緒需要獲取互斥鎖才能執行程式碼) -> 每一個女生都需要經過一段長時間的發展,處成物件(執行緒獲取互斥鎖) -> 男生告訴自己的物件自己沒有老婆(一個執行緒進入if判斷) -> 男生有了老婆(建立物件) -> 返回物件

5、懶漢式(同步方法)

參考程式碼

/**
 * 一個男人只能有一個老婆wife -> Wife類實現單例模式
 * 方式5:懶漢式單例模式(同步方法,執行緒安全,效能差)
 */
public class Wife {
    // 第二步:設定私有靜態屬性
    private static Wife wife = null;

    // 第一步:構造器私有化
    private Wife() {
    }

    // 第三步:向外提供獲取物件的靜態方法
    public synchronized static Wife getInstance() { // 同步方法,原理和同步程式碼塊實現懶漢式相同
        if (wife == null) {
            wife = new Wife();
        }
        return wife;
    }
}

優缺點:和同步程式碼塊實現懶漢式類似,這裡不過多贅述。

6、懶漢式(DCL模式⭐)

DCL模式實現懶漢式單例模式,即雙重檢查機制(DCL, Double Check Lock),執行緒安全,效能高 <- 面試重點

參考程式碼

/**
 * 一個男人只能有一個老婆wife -> Wife類實現單例模式
 * 方式6:懶漢式單例模式(DCL模式 -> 雙重檢查,執行緒安全,效能高)
 */
public class Wife {
    // 第二步:設定私有靜態屬性
    // volatile關鍵字:極大程度上避免JVM底層出現指令重排情況,極端情況除外
    private static volatile Wife wife = null;

    // 第一步:構造器私有化
    private Wife() {
    }

    // 第三步:向外提供獲取物件的靜態方法
    public static Wife getInstance() {
        // 第一層if判斷作用:當物件已經建立好時,直接跳過if語句,返回已經建立好的物件,不在等待獲取互斥鎖 -> 節省時間,提高效能
        if (wife == null) {
            // 注意:這裡容易有多個執行緒同時進入第一層if的程式碼塊中,等待獲取物件鎖
            synchronized (Wife.class) { // 同步程式碼塊,保證每個執行緒進入if判斷前都需要獲得互斥鎖
                // 第二層if判斷作用:當有多個執行緒都進入了第一層if語句內,會出現執行緒1進入時物件為空,則建立物件,釋放互斥鎖,執行緒2獲得互斥鎖後如果沒有第二層if判斷,則直接建立物件,破壞了單例模式 -> 第二層if保證執行緒安全
                if (wife == null) {
                    wife = new Wife();
                    // 在JVM底層建立物件時,大致分為3條指令
                    // 1.分配記憶體空間 -> 2.構造器初始化 -> 3.物件參照指向記憶體空間
                    // JVM為了執行效率,會打亂指令順序(指令重排),有可能是1 -> 3 -> 2
                    // 當執行到3時,物件還沒有建立完成,但是其他執行緒在第一層if判斷已經建立好物件直接返回,顯然不合理(物件屬性還沒有初始化完成) -> 保證指令執行順序不被打亂(保證單條語句編譯後的原子性) -> 使用volatile變數,禁止JVM優化重排指令
                }
            }
        }
        return wife;
    }
}

DCL模式的兩層if判斷的作用:

  • 第一層if:已經建立好物件時直接返回,不再排隊獲取互斥鎖,提升效率
  • 第二層if:保證執行緒安全

通俗的來講:

  1. 有多個女生問同一個男生有沒有老婆?

  2. -> 男生口頭回答說沒有老婆(進入第一層if判斷,如果有老婆則直接遠離:不要去碰一個已婚的男人,他是一個女人的餘生,不是你的男人不要情意綿綿) -> 其中一個女生和男生處成物件(一個執行緒獲取到互斥鎖) -> 經過發展後,女生和男生登記結婚,民政局辦理結婚證時檢查男生婚姻情況(第二層if判斷) --未婚--> 成為夫妻,男生獲得老婆

  3. 獲得老婆資訊

優缺點

  • 優點:效能高,執行緒安全,延時載入
  • 缺點:由於JVM底層模型,volatile不能完全避免指令重排的情況,會偶爾出現問題,反射、序列化會破壞雙檢索單例。

7、懶漢式(靜態內部類)

參考程式碼

/**
 * 一個男人只能有一個老婆wife -> Wife類實現單例模式
 * 方式7:懶漢式單例模式(靜態內部類,執行緒安全,效能高)
 */
public class Husband {
    private Husband() {}

    private static class Wife {
        private static Husband husband = new Husband();
    }

    public static Husband getInstance() {
        return Wife.husband;
    }
}

細節說明

  1. 靜態內部類實現的單例模式同樣是懶漢式 -> 外部類載入時並沒有建立好物件,只有呼叫特定方法時才會載入靜態內部類資訊(內部類的靜態物件屬性建立完畢)
  2. 靜態內部類的方式實現的單例模式:執行緒安全(只有在第一次載入內部類資訊時才會建立物件),效率高(不需要獲取互斥鎖)

優缺點

  • 優點:執行緒安全,效率高,實現了延遲載入
  • 缺點:只適合簡單的物件範例,需要建立的物件範例有複雜操作時(如要對 物件範例 進行其他賦值操作),程式碼會更復雜。反射會破壞單例模式。

8、餓漢式(列舉)

參考程式碼

enum Husband {
    HUSBAND;
}

說明:除了第8種列舉類實現單例模式,其他七種模式都會被反射、序列化破壞單例模式(因為反射可以獲得類的私有屬性的構造器),只有列舉類實現的單例不會被反射破壞,反射無法獲取到列舉類的構造方法。

優缺點

  • 優點:執行緒安全,呼叫效率高,不會被反射、序列化破壞列舉單例
  • 缺點:不能延時載入