五種方式實現 Java 單例模式

  • 2022 年 6 月 17 日
  • 筆記

前言

單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。這種類型的設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。

這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。

餓漢單例

是否多線程安全:是

是否懶加載:否

正如名字含義,餓漢需要直接創建實例。

public class EhSingleton {

    private static EhSingleton ehSingleton = new EhSingleton();

    private EhSingleton() {}

    public static EhSingleton getInstance(){
        return ehSingleton;
    }
}

缺點: 類加載就初始化,浪費內存
優點: 沒有加鎖,執行效率高。還是線程安全的實例。

懶漢單例

懶漢單例,在類初始化不會創建實例,只有被調用時才會創建實例。

非線程安全的懶漢單例

是否多線程安全:否

是否懶加載: 是

public class LazySingleton {

    private static LazySingleton ehSingleton;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (ehSingleton == null) {
            ehSingleton = new LazySingleton();
        }
        return ehSingleton;

    }

}

實例在調用 getInstance 才會創建實例,這樣的優點是不佔內存,在單線程模式下,是安全的。但是多線程模式下,多個線程同時執行 if (ehSingleton == null) 結果都為 true,會創建多個實例,所以上面的懶漢單例是一個線程不安全的實例。

加同步鎖的懶漢單例

是否多線程安全:是

是否懶加載: 是

為了解決多個線程同時執行 if (ehSingleton == null) 的問題,getInstance 方法添加同步鎖,這樣就保證了一個線程進入了 getInstance 方法,別的線程就無法進入該方法,只有執行完畢之後,其他線程才能進入該方法,同一時間只有一個線程才能進入該方法。

public class LazySingletonSync {

    private static LazySingletonSync lazySingletonSync;

    private LazySingletonSync() {}

    public static synchronized LazySingletonSync getInstance() {
        if (lazySingletonSync == null) {
            lazySingletonSync =new LazySingletonSync();
        }
        return lazySingletonSync;
    }

}

這樣配置雖然保證了線程的安全性,但是效率低,只有在第一次調用初始化之後,才需要同步,初始化之後都不需要進行同步。鎖的粒度太大,影響了程序的執行效率。

雙重檢驗懶漢單例

是否多線程安全:是

是否懶加載:是

使用 synchronized 聲明的方法,在多個線程訪問,比如A線程訪問時,其他線程必須等待A線程執行完畢之後才能訪問,大大的降低的程序的運行效率。這個時候使用 synchronized 代碼塊優化執行時間,減少鎖的粒度

雙重檢驗首先判斷實例是否為空,然後使用 synchronized (LazySingletonDoubleCheck.class) 使用類鎖,鎖住整個類,執行完代碼塊的代碼之後,新建了實例,其他代碼都不走 if (lazySingletonDoubleCheck == null) 裏面,只會在最開始的時候效率變慢。而 synchronized 裏面還需要判斷是因為可能同時有多個線程都執行到 synchronized (LazySingletonDoubleCheck.class) ,如果有一個線程線程新建實例,其他線程就能獲取到 lazySingletonDoubleCheck 不為空,就不會再創建實例了。

public class LazySingletonDoubleCheck {

    private static LazySingletonDoubleCheck lazySingletonDoubleCheck;

    private LazySingletonDoubleCheck() {}

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

靜態內部類

是否多線程安全:是

是否懶加載:是

外部類加載時,並不會加載內部類,也就不會執行 new SingletonHolder(),這屬於懶加載。只有第一次調用 getInstance() 方法時才會加載 SingletonHolder 類。而靜態內部類是線程安全的。

靜態內部類為什麼是線程安全

靜態內部類利用了類加載機制的初始化階段 方法,靜態內部類的靜態變量賦值操作,實際就是一個 方法,當執行 getInstance() 方法時,虛擬機才會加載 SingletonHolder 靜態內部類,

然後在加載靜態內部類,該內部類有靜態變量,JVM會改內部生成方法,然後在初始化執行方法 —— 即執行靜態變量的賦值動作。

虛擬機會保證 方法在多線程環境下使用加鎖同步,只會執行一次 方法。

這種方式不僅實現延遲加載,也保障線程安全。

public class StaticClass {

    private StaticClass() {}

    private static class SingletonHolder {
        private static final SingletonHolder INSTANCE = new SingletonHolder();
    }

    public static final SingletonHolder getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

總結

  • 餓漢單例類加載就初始化,在沒有加鎖的情況下實現了線程安全,執行效率高。但是無論有沒有調用實例都會被創建,比較浪費內存。
  • 為了解決內存的浪費,使用了懶漢單例,但是懶漢單例在多線程下會引發線程不安全的問題。
  • 不安全的懶漢單例,使用 synchronized 聲明同步方法,獲取實例就是安全了。
  • synchronized 聲明方法每次線程調用方法,其它線程只能等待,降低了程序的運行效率。
  • 為了減少鎖的粒度,使用 synchronized 代碼塊,因為只有少量的線程獲取實例,實例是null,創建實例之後,後續的線程都能獲取到線程,也就無需使用鎖了。可能多個線程執行到 synchronized ,所以同步代碼塊還需要再次判斷一次。
  • 靜態內部類賦值實際是調用 方法,而虛擬機保證 方法使用鎖,保證線程安全。