1. 程式人生 > 其它 >Java 設計模式之單例模式

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;
    }

}