【設計模式】三、單例模式(10分鐘深度搞定)

馬楚欣發表於2020-10-14

前言

單例模式是保證任何情況下,都僅有一個例項,並提供全域性訪問的方法。

一、餓漢式

先上程式碼

/**
 * 餓漢式
 */
public class Singleton1 {

    private static final Singleton1 instance = new Singleton1();

    /**
     * 私有化構造方法 (防止手動new)
     */
    private Singleton1(){}

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

餓漢式的關鍵在於instance作為類變數直接得到初始化,該方法能夠百分之百的保證同步,也就是說instance在多執行緒下也不可能被例項化兩次,但是instance被ClassLoader載入後可能很長時間才會被使用,那就意味著instance例項所開闢的空間的堆記憶體會駐留更久的時間。
如果一個類中的成員屬性比較少,所佔用的記憶體資源不多,餓漢式也未嘗不可。總結起來,餓漢式可以保證多執行緒下唯一的例項,getInstance效能也比較高,但是無法進行懶載入

二、懶漢式

/**
 * 懶漢式
 */
public class Singleton2 {

    private static  Singleton2 instance = null;

    /**
     * 私有化構造方法
     */
    private Singleton2() {
    }

    // 存線上程安全問題
    public static Singleton2 getInstance() {
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }
}

Singleton2 的類變數instance = null,當Singleton2.class被初始化的時候instance並不會被例項化,在getInstance方法中會判斷instance 例項是否被例項化,看起來沒什麼問題,但在多執行緒環境下,會導致instance可能被例項化多次。 執行緒1判斷null == instance為true時,還沒有例項化instance,切換到了執行緒2執行,執行緒2判斷null == instance也為true。就會例項化多次。

三、懶漢式 + 同步

// 解決執行緒安全問題,但是效率低
    public synchronized static Singleton2 getInstance2() {
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }

採用懶漢式 + 資料同步方式既滿足了懶載入又能百分之百保證instance例項的唯一性,但是synchronized 關鍵字天生的排他性導致了getInstance方法只能在同一時刻被一個執行緒所訪問,效能低下。

四、懶漢式 + Double-Check

// 解決執行緒安全問題,提升效率
    private static  Singleton2 instance = null;
    private String msg;
    
    /**
     * 私有化構造方法
     */
    private Singleton2() {
        msg = "初始化引數";
    }
    public static Singleton2 getInstance3() {
        if (instance == null) {
            synchronized (Singleton2.class){
                if(instance == null){
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }

當兩個執行緒發現null == instance成立時,只有一個執行緒有資格進入同步程式碼塊,完成對instance的例項化,隨後的執行緒發現 null == instance 不成立則無須進行任何操作,以後對getInstance的訪問就不需要資料同步的保護了。

這種方式看起來那麼的完美,既滿足了懶載入,有保證instance例項的唯一性。Double-Check的方式提供了高效的資料同步策略,可以允許多個執行緒同時對getInstance進行訪問。但是這種方式有可能引起空指標異常,我們分析一下。

Singleton4的建構函式中,初始化了msg還有Singleton2自身,根據JVM指令重排序和Happens-Before規則,這兩者之間的例項化順序並無前後關係的約束,那麼極有可能instance最先被例項化,而msg並未完成例項化,未完成初始化的例項呼叫其他方法將會丟擲空指標異常。

五、Volatile + Double + Check

 private volatile static  Singleton2 instance = null;
 private String msg;
    
    /**
     * 私有化構造方法
     */
    private Singleton2() {
        msg = "初始化引數";
    }
    public static Singleton2 getInstance3() {
        if (instance == null) {
            synchronized (Singleton2.class){
                if(instance == null){
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }

在instance前 加上 volatile的關鍵字,則可以防止重排序的發生。但終歸加了synchronized,對效能依舊造成了影響。有沒有更好的方式呢?有!

六、Holder方式

public class Singleton3 {

    private static Singleton3 instance = null;

    private static class Holder{
        private static final Singleton3 singleton3 = new Singleton3();
    }


    /**
     * 私有化構造方法
     */
    private Singleton3() {
    }

    public static final Singleton3 getInstance() {
        return Holder.singleton3;
    }
    
}

在Singleton6中並沒有instance的靜態變數,而是將其放在靜態內部類Holder類中,因此Singleton3初始化過程中並不會建立Singleton3的例項,Holder類中定義了Singleton3的靜態變數,並且直接進行了例項化,當Holder被直接引用的時候則會建立Singleton3的例項,該方法又是同步方法,保證了記憶體的可見性,JVM的順序性和原子性。Holder方式是單例設計最好的設計之一。但是!依然優缺點。

反射破壞單例

public static void main(String[] args) throws Exception {
        // 無聊情況下進行破壞
        Class<?> clazz = Singleton3.class;
        // 獲取私有化構造方法
        Constructor constructor = clazz.getDeclaredConstructor(null);
        // 強制訪問
        constructor.setAccessible(true);
        // 暴力初始化兩次
        Object o1 = constructor.newInstance();
        Object o2 = constructor.newInstance();
        System.out.println(o1);
        System.out.println(o2);
        System.out.println(o1 == o2);
    }

執行結果
在這裡插入圖片描述
顯然我們建立出來了兩個例項。破壞了我們的初衷。我們來優化一次,在構造方法做限制,一旦重複建立,我們就拋異常。

public class Singleton3 {

    private static class Holder{
        private static final Singleton3 singleton3 = new Singleton3();
    }

    /**
     * 私有化構造方法
     */
    private Singleton3() {
        if(Holder.singleton3 != null){
            throw new RuntimeException("不允許建立多個例項");
        }
    }

    public static final Singleton3 getInstance() {
        return Holder.singleton3;
    }

}

感覺上該單例已經完美了,然而還有可能被破壞。

序列化破壞單例

實現序列化

public class Singleton4 implements Serializable {

    private static class Holder{
        private static final Singleton4 singleton3 = new Singleton4();
    }

    /**
     * 私有化構造方法
     */
    private Singleton4() {
        if(Holder.singleton3 != null){
            throw new RuntimeException("不允許建立多個例項");
        }
    }

    public static final Singleton4 getInstance() {
        return Holder.singleton3;
    }

}

測試程式碼

		Singleton4 s1 = null;
        Singleton4 s2 = Singleton4.getInstance();

        FileOutputStream fos = new FileOutputStream("Singleton4.obj");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(s2);
        oos.flush();
        oos.close();

        FileInputStream fis = new FileInputStream("Singleton4.obj");
        ObjectInputStream ois = new ObjectInputStream(fis);
        s1 = (Singleton4) ois.readObject();
        ois.close();

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

執行結果
在這裡插入圖片描述
顯然,單例又遭到破壞。如何解決呢?只需要新增readResolve 方法即可。

public class Singleton4 implements Serializable {

    private static class Holder{
        private static final Singleton4 singleton3 = new Singleton4();
    }

    /**
     * 私有化構造方法
     */
    private Singleton4() {
        if(Holder.singleton3 != null){
            throw new RuntimeException("不允許建立多個例項");
        }
    }

    public static final Singleton4 getInstance() {
        return Holder.singleton3;
    }

    private Object readResolve(){
        return Holder.singleton3;
    }

}

再看執行效果
在這裡插入圖片描述
有興趣的同學可以檢視JDK的原始碼,發現實際上,這裡我們還是例項化了兩次,只不過第二次建立的物件沒有被返回而已。這樣也會造成記憶體的不必要浪費。

七、註冊式單例

註冊時單例就是將每個例項登記到某個地方,使用唯一標識來獲取例項。

列舉式單例

public enum Singleton5  {

    INSTANCE;
    private Object data;


    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public static final Singleton5 getInstance() {
        return INSTANCE;
    }
}

驚喜的是,列舉類天生就防止反射破壞與序列化破壞,有興趣的同學,可以查閱JDK原始碼。

容器式單例

列舉類雖然寫法優雅,但是在類載入之時,就將所有物件初始化放在記憶體中,這其實與餓漢式無異。容器式則是將Bean 放在 concurrentHashMap<String,Object>中,詳細可參照Spring IOC的實現,這裡就不多做敘述了。

原始碼地址: https://gitee.com/xiaowangz/learning-note.git

相關文章