五種方式實現 Java 單例模式

小碼code 發表於 2022-06-17
Java

前言

單例模式(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 ,所以同步程式碼塊還需要再次判斷一次。
  • 靜態內部類賦值實際是呼叫 方法,而虛擬機器保證 方法使用鎖,保證執行緒安全。