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

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 方法,會將此方法的返回值返回為反序列化的對象,可以通過該機制處理反序列化破壞單例的隱患。