常用設計模式-單例模式

加了冰的才叫可乐發表於2024-04-26

單例模式 (保證只有一個例項,並只提供一個訪問例項的訪問點)

單例模式的建立方式:

  • 餓漢模式-靜態變數
package com.pattern;

//餓漢模式-靜態變數
public class Singleton {
    
    // 類初始化時,會立即載入該物件,執行緒天生安全,呼叫效率高
    private static final Singleton singleton = new Singleton();
    
    private Singleton() {
        System.out.println("私有Singleton構造引數初始化");
    }
    
    public static Singleton getInstance() {
        return singleton;
    }
}

由於使用了static關鍵字,保證了在引用這個變數時,關於這個變數的所有寫入操作已經完成,所以保證了JVM層面的執行緒安全。

  • 餓漢模式-靜態程式碼塊

package com.pattern;

//餓漢模式-靜態程式碼塊
public class Singleton {
    
    private static  Singleton singleton;
    
    private Singleton() {
        System.out.println("私有Singleton構造引數初始化");
    }
    
    static {
        try {
            //Do something ... //new 放在static程式碼塊裡可初始化一些變數或者讀取一些配置檔案等
             singleton = new Singleton()
        }catch (Exception e){
            throw new RuntimeException("Exception occured in creating singleton instance");
        }
    }
    public static Singleton getInstance() {
        return singleton;
    }
}
  • 懶漢式-單執行緒
package com.singleton;

//懶漢式-單執行緒
public class Singleton {
    
    //類初始化時,不會初始化該物件,真正需要使用的時候才會建立該物件,具備懶載入功能
    private static Singleton singleton;

    private Singleton() {
        System.out.println("私有Singleton構造引數初始化");
    }
    
    public  static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

只適用於單執行緒場景,多執行緒情況下可能發生執行緒安全問題,導致建立不同例項的情況發生。如果是多執行緒同時呼叫getInstance(),會有併發問題啊,多個執行緒可能同時拿到instance == null的判斷,這樣就會重複例項化,單例就不是單例。

解決見下面

  • 懶漢式-synchronized
package com.singleton;

//懶漢式-synchronized
public class Singleton {
    
    private static Singleton singleton;

    private Singleton() {
        System.out.println("私有Singleton構造引數初始化");
    }
    //synchronized 保證了同步訪問該方法,嚴格序列制,效能降低
    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}
  • 懶漢式-Double check-volatile
package com.singleton;

//懶漢式-Double check-volatile
public class Singleton {
    
    private static volatile Singleton singleton;

    private Singleton() {
        System.out.println("私有Singleton構造引數初始化");
    }
    //該方式透過縮小同步範圍提高訪問效能,同步程式碼塊控制併發建立例項。採用雙重檢驗(內外兩個判空)
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

內層的判空的作用:

當兩個執行緒同時執行第一個判空時,都滿足的情況下,都會進來,然後去爭鎖,假設執行緒1拿到了鎖,執行同步程式碼塊的內容,建立了例項並返回,釋放鎖,然後執行緒2獲得鎖,執行同步程式碼塊內的程式碼,因為此時執行緒1已經建立了,所以執行緒2雖然拿到鎖了,如果內部不加判空的話,執行緒2會再new一次,導致兩個執行緒獲得的不是同一個例項。執行緒安全的控制其實是內部判空在起作用。

可以只加內層判空是ok的

外層的判空的作用:

  • 內層判空已經可以滿足執行緒安全了,加外層判空的目的是為了提高效率。
  • 因為可能存在這樣的情況:如果不加外層判空,執行緒1拿到鎖後執行同步程式碼塊,在new之後,還沒有釋放鎖的時候,執行緒2過來了,它在等待鎖(此時執行緒1已經建立了例項,只不過還沒釋放鎖,執行緒2就來了),然後執行緒1釋放鎖後,執行緒2拿到鎖,進入同步程式碼塊中,判空不成立,直接返回例項。
  • 這種情況執行緒2是不是不用去等待鎖了?因為執行緒1已經建立了例項,只不過還沒釋放鎖。
  • 所以在外層又加了一個判空就是為了防止這種情況,執行緒2過來後先判空,不為空就不用去等待鎖了,這樣提高了效率。

volatile作用:

  • 在多執行緒的情況下,雙重檢查鎖模式可能會出現空指標問題,出現問題的原因是JVM在例項化物件的時候會進行最佳化和指令重排序操作,因為new那行程式碼並不是一個原子指令,會被分割成多個指令。
  例項化物件實際上可以分解成以下4個步驟:
    1. 為物件分配記憶體空間
    2. 初始化預設值(區別於構造器方法的初始化)
    3. 執行構造器方法
    4. 將物件指向剛分配的記憶體空間
  編譯器或處理器為了效能的原因,可能會將第3步和第4步進行重排序:
    1. 為物件分配記憶體空間
    2. 初始化預設值
    3. 將物件指向剛分配的記憶體空間
    4. 執行構造器方法執行緒可能獲得一個初始化未完成的物件

  • 靜態內部類
package com.singleton;

// 靜態內部類方式
public class Singleton {
    
    //結合了懶漢式和餓漢式各自的優點,真正需要物件的時候才會載入,載入類是執行緒安全的。
    private Singleton() {
        System.out.println("私有Singleton構造引數初始化");
    }
    
    public static class SingletonClassInstance {
        private static final Singleton singleton = new Singleton();
    }
    
    // 方法沒有同步
    public static Singleton getInstance() {
        return Singleton.singleton;
    }
}

該方式是執行緒安全的,適用於多執行緒,利用了java內部類的特性:

  靜態內部類不會自動隨著外部類的載入和初始化而初始化,內部類是要單獨載入和初始化的。此方式單例物件是在內部類載入和初始化時才建立的,因此它是執行緒安全的,且實現了延遲初始化

  • 列舉單例式

列舉是最簡潔的,不需要考慮構造方法私有化。

值得注意的是列舉類不允許被繼承,因為列舉類編譯後預設為final class,可防止被子類修改。

。列舉類是利用JVM類載入機制來保證執行緒安全的(細節見這篇),並且只會裝載一次,設計者充分的利用了列舉的這個特性來實現單例模式,列舉的寫法非常簡單,而且列舉型別是所用單例實現中唯一一種不會被破壞的單例實現模式。

package com.singleton;

// getInstance()訪問效能高,執行緒安全    非延遲初始化
public enum EnumSingleton {

    INSTANCE;
    
    private Resource instance;
    
    EnumSingleton(){
    	//doSomething();
        instance = new Resource();
    }
    
    public Resource getInstance() {
        return instance;
    }
    
}

public class EnumSingletonEnumTest {
    public static void main(String[] args) {
        Resource instance = EnumSingleton.INSTANCE.getInstance();
        System.out.println(instance);
    }
}

破壞單例模式的方法和防範措施

反射是透過強行呼叫私有構造方法生成新的物件。

防範方法

如果我們想要阻止單例破壞,可以在構造方法中進行判斷,若已有例項,則阻止生成新的例項。

private Singleton(){
    if (instance != null){
        throw new RuntimeException("例項已經存在,請透過 getInstance()方法正確獲取");
    }
}

序列化和單例模式

有時在分散式系統中,我們需要在單例類中實現可序列化介面,這樣我們就可以在檔案系統中儲存它的狀態,並在以後的時間點檢索它。

每當反序列化它時,它都會建立該類的新例項。對比其hashCode值不一致

防範方法
  1. 不實現序列化介面
  2. 如果必須實現序列化介面,可以重寫反序列化方法readResolve(),反序列化時直接返回相關單例物件。
protected Object readResolve() {
    return getInstance();
}

cloneable介面的破壞

和可序列化介面有些類似,當需要實現單例的類允許clone()時,如果處理不當,也會導致程式中出現不止一個例項。

防範方法
重寫clone()方法,調clone()時直接返回已經例項的物件。
protected Object clone() throws CloneNotSupportedException {
        return instance;
}

相關文章