23種設計模式-單例設計模式介紹帶實戰

2022-10-28 18:01:15

1、描述

確保一個類只有一個範例,並提供對該範例的全域性存取。如果你建立了一個物件, 同時過一會兒後你決定再建立一個新物件, 此時你會獲得之前已建立的物件, 而不是一個新物件。

這種模式涉及到一個單一的類,該類負責建立自己的物件,同時確保只有單個物件被建立。這提供了一種存取其唯一的物件的方式,可以直接存取,不需要範例化該類的物件。

2、實現邏輯

  • 私有化構造方法
  • 提供唯一的公共的獲取物件方法

3、實戰程式碼

3.1 餓漢式單例模式

/**
 * 餓漢式單例模式 demo
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-10-27 16:17:50
 */
public class HungrySingleton {

    private static HungrySingleton singleton = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return singleton;
    }
}

這種實現方式 instance 物件在類載入時建立,天然的執行緒安全,但是如果該物件足夠大的話,而且不是必須使用的會造成記憶體浪費。且 GC 時無法回收。

3.2 懶漢式單例模式

/**
 * 懶漢式單例模式
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-10-28 14:02:22
 */
public class LazySingleton {

    private static LazySingleton instance;

    private LazySingleton() {
    }

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

        return instance;
    }
}

這種方式顯而易見我們在第一次存取 getInstance() 時,才開始建立物件,解決上面餓漢式不使用時也佔用記憶體的問題,但是又出現了個新的問題,在多執行緒的情況下,會出現執行緒安全問題。

3.3 懶漢式單例模式加鎖

/**
 * 執行緒安全的懶漢式單例模式
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-10-28 14:31:14
 */
public class SynLazySingleton {
    private static SynLazySingleton instance;

    private SynLazySingleton() {
    }

    public static synchronized SynLazySingleton getInstance() {
        if (instance == null) {
            instance = new SynLazySingleton();
        }
        return instance;
    }
}

為了解決執行緒安全問題,最簡單的處理,直接在存取方法新增 synchronized 關鍵字,這樣每個執行緒都必須持有鎖才能存取。但是對於 getInstance() 方法來說,只有在建立物件時才會導致執行緒安全問題,在第一次存取建立物件後的後續存取是不需要加鎖的,為了提高方法後續存取效能,我們需要調整加鎖的時機。由此也產生了一種新的實現模式:雙重檢查鎖模式

3.4 雙重檢查鎖模式

/**
 * 雙重檢查鎖單例模式
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-10-28 14:40:42
 */
public class DoubleCheckLockSingleton {
    private static volatile DoubleCheckLockSingleton instance;

    private DoubleCheckLockSingleton() {
    }

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

同時我們為了防止 JVM 在範例化物件的時候會進行優化和指令重排序操作時導致的空指標問題,我們需要使用 volatile 關鍵字,來保證可見性和有序性。這樣我們就優雅的解決了單例記憶體漏失執行緒安全還有效能的問題了。

3.5 靜態內部類模式

利用 JVM 在載入外部類的時不會載入靜態內部類, 只有內部類的屬性/方法被呼叫時才會被載入, 並初始化其靜態屬性的機制。靜態屬性由於被 static 修飾,保證只被範例化一次,並且嚴格保證範例化順序。

/**
 * 靜態內部類模式
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-10-28 15:07:59
 */
public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton() {
    }

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


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

靜態內部類單例模式是一種優秀的單例模式,是開源專案中比較常用的一種單例模式。在沒有加任何鎖的情況下,保證了多執行緒下的安全,並且沒有任何效能影響和空間的浪費。

3.6 列舉方式

在日常開發中,我們經常遇到的列舉也屬於餓漢式單例模式的實現,在 JVM 類載入時載入,天然的執行緒安全,且只會被載入一次。

4、如何破壞單例

  • 反射
  • 序列化

4.1 反射破壞單例模式

我們知道單例的本質就是私有化構造方法,然後通過單例類提供的公共方法來獲取唯一物件。但是私有化後的構造方法能通過反射輕鬆獲取到,然後執行。

/**
 * 反射破壞單例模式
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-10-28 15:18:49
 */
public class ReflectionDamage {
    public static void main(String[] args) throws Exception {
        //獲取類的位元組碼物件
        Class clazz = DoubleCheckLockSingleton.class;
        //獲取類的私有無參構造方法物件
        Constructor constructor = clazz.getDeclaredConstructor();
        //取消存取檢查
        constructor.setAccessible(true);
        DoubleCheckLockSingleton s1 = (DoubleCheckLockSingleton) constructor.newInstance();
        DoubleCheckLockSingleton s2 = (DoubleCheckLockSingleton) constructor.newInstance();

        System.out.println(s1 == s2);
    }
}

得到結果 false

4.2 序列化和反序列化

將物件序列化後再反序列化得到的物件在堆中肯定不是相同地址,而且反序列化也能得到多個物件。明顯破壞了單例的模式。

但是反序列化時如果該物件類中存在 readResolve 方法,會將此方法的返回值返回為反序列化的物件,可以通過該機制處理反序列化破壞單例的隱患。