Java 設計模式之單例模式

單例模式

定義:保證一個類有且僅有一個實例,並提供一個全局訪問點

適用場景:想確保任何情況下都絕對只有一個實例

優點

  • 在內存里只有一個實例,減少了內存開銷
  • 可以避免對資源的多重佔用
  • 設置全局訪問點,嚴格控制訪問

缺點:沒有接口,擴展困難

特點

  • 私有構造器(即被 private 修飾構造方法)
  • 線程安全
  • 延遲加載
  • 序列化和反序列化安全、
  • 反射

餓漢式單例

餓漢式單例是類進行初始化的時候,就已經把對象創建好了,並且使用 final 修飾,因為 final 關鍵字在類初始化時就必須把變量初始化好,並且不可改變,很符合單例模式的特徵。

public class HungrySingleton {

    private final static HungrySingleton instance;

    static {
        instance = new HungrySingleton();
    }

    private HungrySingleton() {
    }

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

懶漢式單例

注重的是 延時加載 ,就意味着只有在使用它的時候,才開始初始化,不使用則不會初始化,
以下是線程不安全的懶漢式單例模式代碼示例

/**
 * @author Hyxiao
 * @date 2022/3/15 17:02
 * @description 單例模式-懶漢模式(懶->初始化的時候沒有創建對象)
 */
public class LazySingleton {

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

}

線程安全的懶漢式單例模式代碼示例

public class LazySingleton {

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

}

需要留意的是,當用 synchronized 修飾靜態 static 方法時,相當於鎖的是 LazySingleton 的class文件,也就是把這個類給鎖住了;而用 synchronized 修飾普通方法時,鎖的是在堆內存中生成的對象。

等同於以下這種寫法(鎖住了整個類)

public class LazySingleton {

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

}

雙重檢查懶漢式單例

public class LazyDoubleCheckSingleton {

    private static LazyDoubleCheckSingleton instance = null;

    private LazyDoubleCheckSingleton() {}

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

}

這種寫法有隱患,具體體現在 if (instance == null) {instance = new LazyDoubleCheckSingleton();這兩個地方

程序會先對進來的 instance 對象進行判空,會出現 instance 不為 null 的情況,這時候雖然 instance 是不為 null 的,但是 instance 還沒有完成初始化的過程,就是 instance = new LazyDoubleCheckSingleton(); 沒有執行完。針對這種 instance 會為 null 實例的場景,為此我們提出兩種解決方案,一種是 保證類初始化過程的有序性 ,另一種是 類初始化時,隔離其他線程干擾

通常當我們創建並初始化一個 LazyDoubleCheckSingleton 對象時,正常情況下要經歷以下三個步驟:

instance = new LazyDoubleCheckSingleton();
  1. 分配內存給這個對象
  2. 初始化對象
  3. 設置 instance 指向步驟 1 剛分配好的內存地址

但是按上面所說的特殊情況,程序可能會碰到當執行完步驟 1 後,步驟 2 和 3 很有可能會出現順序顛倒,也就是重排序,也就是下面這種情況

  1. 分配內存給這個對象
  2. 設置 instance 指向步驟 1 剛分配好的內存地址
  3. 初始化對象

所以,當出現重排序情況時, 也就是 instance 已經指向分配好的內存地址,但是 instance 它是沒有初始化完成的。也就是說在多線程並發的情況下,其他線程進來拿到 instance ,由於 instance 已經分配好了內存地址,所以 instance 不為 null ,就直接返回 instance 這個沒有初始化的實例,系統就會報異常。

而對於單線程情況下,這種重排序的特殊情況,是不會有什麼影響的,不會改變程序的執行結果,Java 語言規範是允許那些在單線程內不會改變單線程程序執行結果的重排序,因為單線程下的重排序,反而能提高執行性能。

保證類初始化過程的有序性

為了避免出現這種步驟 2 和 3 重排序的問題,我們可以通過 volatile 關鍵字來修飾 instance 來實現線程安全的延遲初始化,從而禁止重排序。如下示例代碼,在聲明 instance 實例時採用 volatile 來修飾,來保證步驟 2 和 3 的有序執行,防止出現重排序

private volatile static LazyDoubleCheckSingleton instance = null;

類初始化時,隔離其他線程干擾

除了使用 volatile 來限制重排序以外,我們還能通過靜態內部類的方式;因為 JVM 在類的初始化階段,會去獲取一個鎖,這個鎖會同步多個線程對一個類的初始化。這樣當其中一個線程在創建並初始化一個單例類的時候,其他線程是無法得知這個類的具體情況的,這也就保證了即使出現重排序,但是其他線程也無法獲得到這個類的實例。

public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton(){}
    
    private static class InnerClass {
        private static StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.innerClassSingleton;
    }

}
Tags: