Java設計模式【單例模式】

2023-05-12 18:01:00

Java設計模式【單例模式】

單例模式

單例模式(Singleton Pattern)是一種建立型設計模式,其主要目的是確保一個類只有一個範例,並提供對該範例的唯一存取點。

優缺點

優點:

  1. 提供了對唯一範例的受控存取。

  2. 由於在系統記憶體中只存在一個物件,因此可以節約系統資源。

缺點

  1. 單例類的擴充套件有很大的困難。

  2. 單例類的職責過重,在一定程度上違背了「單一職責原則」。

  3. 物件生命週期。 單例模式沒有提出物件的銷燬,在提供記憶體的管理的開發語言中,只有單例模式物件自己才能將物件範例銷燬,因為只有它擁有對範例的參照。 在各種開發語言中,比如C++,其他類可以銷燬物件範例,但是這麼做將導致單例類內部的指標指向不明。

單例模式的使用

餓漢模式

  1. 靜態成員變數
/**
 * @author Physicx
 * @date 2023/5/12 下午10:13
 * @desc 單例
 * Created with IntelliJ IDEA
 */
public class Singleton {

    //初始化範例物件
    private static final Singleton instance = new Singleton();

    //私有化構造方法
    private Singleton() {
    }

    //提供獲取範例物件方法
    public static Singleton getInstance() {
        return instance;
    }

}
  1. 靜態程式碼塊
/**
 * @author Physicx
 * @date 2023/5/12 下午10:13
 * @desc 單例
 * Created with IntelliJ IDEA
 */
public class Singleton {

    //範例物件
    private static final Singleton instance;

    static {
        instance = new Singleton();
    }

    //私有化構造方法
    private Singleton() {
    }

    //提供獲取範例物件方法
    public static Singleton getInstance() {
        return instance;
    }

}

餓漢式單例的寫法適用於單例物件較少的情況,這樣寫可以保證絕對的執行緒安全,執行效率比較高。但是缺點也很明顯,餓漢式會在類載入的時候就將所有單例物件範例化,這樣系統中如果有大量的餓漢式單例物件的存在,系統初始化的時候會造成大量的記憶體浪費,換句話說就是不管物件用不用,物件都已存在,佔用記憶體。

懶漢模式

public class Singleton {

    //範例物件
    private static Singleton instance;

    //私有化構造方法
    private Singleton() {
    }

    //提供獲取範例物件方法(執行緒安全)
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

}

執行緒安全的一種懶漢式寫法,在類第一次使用的時候初始化,獲取範例的靜態方法由synchronized修飾,所以是執行緒安全的。這種方法每次獲取範例物件都加鎖同步,效率較低。

雙重檢測機制(DCL)

public class Singleton {

    //範例物件
    private static volatile Singleton instance;

    //私有化構造方法
    private Singleton() {
    }

    //提供獲取範例物件方法
    public static Singleton getInstance() {
        if (instance == null) {
            //加鎖處理
            synchronized (Singleton.class) {
                if (instance==null) {
                    //初始化
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

}

範例物件必須用 volatile 修飾,否則極端情況可能出現安全隱患。

以上初始化物件程式碼被編譯後會變成以下三條指令:

  1. 分配物件的記憶體空間。

  2. 初始化物件。

  3. 設定instance指向剛才分配的記憶體空間。

如果按照上面的執行順序則不加volatile沒有問題,但是CPU或編譯器為了提高效率,可能會進行指令重排,最終順序變為:

  1. 分配物件的記憶體空間。

  2. 設定instance指向剛才分配的記憶體空間。

  3. 初始化物件。

當兩個執行緒同時獲取範例物件時,執行緒A已經將instance指向分配空間但未初始化物件,執行緒B此時第一次判空已不為空,於是返回instance範例,但是此時返回的範例未初始化會導致後續空指標異常。

DCL這種方式同樣也是類第一次使用的時候初始化,初始化程式碼synchronized修飾執行緒安全,這種方式只會第一次範例物件才會進行同步,因此效率高。

《Java Concurrency in Practice》作者Brian Goetz在書中提到關於DCL的觀點:促使DCL模式出現的驅動力(無競爭同步的執行速度很慢,以及JVM啟動時很慢)已經不復存在,因而它不是一種高效的優化措施。延遲初始化佔位類模式(靜態內部類)能帶來同樣的優勢,並且更容易理解。

靜態內部類(延遲初始化)

public class Singleton {

    //私有化構造方法
    private Singleton(){}

    //靜態內部類(被呼叫時載入)
    private static class SingletonHandle {
        private static final Singleton instance = new Singleton();
    }

    //提供獲取範例物件方法
    public static Singleton getInstance() {
        return SingletonHandle.instance;
    }

}

利用靜態內部類被呼叫時才載入的特性,通過靜態初始化初始Singleton物件,由於JVM將在初始化期間獲得一個鎖,並且每個執行緒都至少獲取一次這個鎖以確保這個類已經載入,因此在靜態初始化期間,記憶體寫入操作將自動對所有執行緒可見。因此無論是在被構造期間還是被參照時,靜態初始化的物件都不需要顯式的同步。

執行緒安全,效率高,使用的時候才會初始化不浪費記憶體。

《Java Concurrency in Practice》作者Brian Goetz 推薦這種單例實現方式。

列舉實現方式

除了以上幾種常見的實現方式之外,Google 首席 Java 架構師、《Effective Java》一書作者、Java集合框架的開創者Joshua BlochEffective Java一書中提到:單元素的列舉型別已經成為實現Singleton的最佳方法

在這種實現方式中,既可以避免多執行緒同步問題;還可以防止通過反射和反序列化來重新建立新的物件。

public class Singleton {

    //私有化構造方法
    private Singleton() {}

    enum SingletonEnum {
        SINGLETON;
        private final Singleton instance;

        SingletonEnum() {
            instance = new Singleton();
        }
        //提供獲取範例物件方法
        public Singleton getInstance() {
            return instance;
        }
    }

}

呼叫方式如下:

public static void main(String[] args) {
        Singleton instance1 = Singleton.SingletonEnum.SINGLETON.getInstance();
        Singleton instance2 = Singleton.SingletonEnum.SINGLETON.getInstance();
        System.out.println(instance2 == instance1);
    }

普通的單例模式是可以通過反射和序列化/反序列化來破解的,jvm虛擬機器器會保證列舉型別不能被反射並且建構函式只被執行一次,而Enum由於自身的特性問題,是無法破解的。當然,由於這種情況基本不會出現,因此我們在使用單例模式的時候也比較少考慮這個問題。

總結

實現方式 優點 缺點
餓漢模式 執行緒安全,效率高 非懶載入
懶漢模式 執行緒安全,懶載入 效率低
雙重檢測機制 執行緒安全,懶載入,效率高
靜態內部類 執行緒安全,懶載入,效率高
列舉 執行緒安全,效率高 非懶載入

由於單例模式的列舉實現程式碼比較簡單,而且又可以利用列舉的特性來解決執行緒安全和單一範例的問題,還可以防止反射和反序列化對單例的破壞,因此在很多書和文章中都強烈推薦將該方法作為單例模式的最佳實現方法

參考:單例模式詳解(知乎文章)

設計模式相關其他文章:
Java設計模式總結