單例模式看這一篇就夠了

爲什麼要使用單例模式

  1. 對於頻繁使用的對象,可以省略創建對象所花費的時間,這對於那些重量級對象而言,是非常可觀的一筆系統開銷。
  2. 由於new操作的次數減少,所以系統內存的使用評率也會降低,這將減少GC壓力,縮短GC停頓時間。

懶漢模式與餓漢模式

餓漢模式及代碼實現

餓漢模式通過static修飾符修飾,以及構造函數的私有化實現單例模式

public class Singleton {
    private static Singleton singleton = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return singleton;
    }
}

由於使用了static修飾符,在類加載的時候就完成了初始化,所以餓漢模式是線程安全的,但就因爲類加載的時候就完成了初始化,沒有懶加載的效果所以會浪費內存

當Java程序需要使用某個類時,如果該類還未被加載到內存中,JVM會通過加載、連接(驗證、準備和解析)、初始化三個步驟來對該類進行初始化。

類的加載是指把類的.class文件中的數據讀入到內存中,通常是創建一個字節數組讀入.class文件,然後產生與所加載類對應的Class對象。加載完成後,Class對象還不完整,所以此時的類還不可用。當類被加載後就進入連接階段,這一階段包括驗證、準備(爲靜態變量分配內存並設置默認的初始值)和解析(將符號引用替換爲直接引用)三個步驟。最後JVM對類進行初始化,包括:

1)如果類存在直接的父類並且這個類還沒有被初始化,那麼就先初始化父類;

2)如果類中存在初始化語句,就依次執行這些初始化語句。

懶漢模式及代碼實現

懶漢模式在調用的時候才進行new的操作,從而節約了內存,懶漢式總共有三種實現方式,即

  1. 雙重檢查鎖
  2. 靜態內部類
  3. 枚舉

雙重檢查鎖方式

public class Singleton {
    private static Singleton singleton = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (null == singleton) {
            return new Singleton();
        }
        return singleton;
    }
}

上述的代碼有一個致命的問題,在併發操作中,多個線程同時調用獲得實例的方法時,極有可能出現new多次取得不同實例的問題。
爲了解決這個問題,其實最簡單的辦法就是加鎖,加在方法上是最直接有效的,如下

    public static synchronized Singleton getInstance() {
        if (null == singleton) {
            return new Singleton();
        }
        return singleton;
    }

但是會產生效率問題,即new的操作只會進行一次,而加在方法上,同時只能有一個線程來獲取實例。所以可以改爲下面的方式

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

這種方式又稱爲雙重檢查鎖。到目前爲止看似沒有問題,其實還是有潛在的隱患,即指令重排,因爲new操作並不是一個原子性的操作,它分爲一下三個步驟
memory = allocate(); //1:分配對象的內存空間
ctorInstance(memory); //2:初始化對象
instance = memory; //3:設置instance指向剛分配的內存地址
而指令重排之後變爲
memory = allocate(); //1:分配對象的內存空間
instance = memory; //3:設置instance指向剛分配的內存地址
ctorInstance(memory); //2:初始化對象
這樣通過getInstance取得實例可能只分配了內存地址,而並沒有初始化完成即調用屬性方法時會拋異常出來(大概是NoSuchMethodException…懶了不跑了)。
所以完整版雙重檢查鎖模式如下

public class Singleton {
    // 使用volatile關鍵字,即內存屏障功能
    // 相當於禁止使用CPU高速緩存,從而使每次修改前都要同步到主內存中避免了亂序執行的可能保證了併發操作的可見性
    private static volatile Singleton singleton = null;

    private Singleton() {
    }

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

靜態內部類方式

靜態內部類方式

public class StaticSingleton {

    // 由於StaticSingleton沒有靜態成員,所以體現出了懶漢式思想
    private StaticSingleton() {
    }

    // JVM在類加載的時候是互斥的,所以是線程安全的
    private static class SingletonBuilder {
        private static StaticSingleton staticSingleton = new StaticSingleton();
    }

    public static  StaticSingleton getInstance() {
        return SingletonBuilder.staticSingleton;
    }
}

枚舉單例寫法(最簡單最安全)

public enum EnumSingleton {
    INSTANCE;
}

如何破壞一個單例

反射攻擊

直接上代碼:

    public static void reflectionAttack() throws Exception {
        //通過反射,獲取單例類的私有構造器
        Constructor constructor = DoubleCheckLockSingleton.class.getDeclaredConstructor();
        //設置私有成員的暴力破解
        constructor.setAccessible(true);
        // 通過反射去創建單例類的多個不同的實例
        DoubleCheckLockSingleton s1 = (DoubleCheckLockSingleton) constructor.newInstance();
        // 通過反射去創建單例類的多個不同的實例
        DoubleCheckLockSingleton s2 = (DoubleCheckLockSingleton) constructor.newInstance();
        s1.tellEveryone();
        s2.tellEveryone();
        System.out.println(s1 == s2);
    }

執行結果如下:
This is a DoubleCheckLockSingleton 1368884364
This is a DoubleCheckLockSingleton 401625763
false
這種方法非常簡單暴力,通過反射侵入單例類的私有構造方法並強制執行,使之產生多個不同的實例,
這樣單例就被破壞了。要防禦反射攻擊,只能在單例構造方法中檢測instance是否爲null,如果已不爲
null,就拋出異常。顯然雙重檢查鎖實現無法做這種檢查,靜態內部類實現則是可以的。
注意,不能在單例類中添加類初始化的標記位或計數值(比如 boolean flag 、 int count )來
防禦此類攻擊,因爲通過反射仍然可以隨意修改它們的值。
序列化攻擊
這種攻擊方式只對實現了Serializable接口的單例有效,但偏偏有些單例就是必須序列化的。現在假設
DoubleCheckLockSingleton類已經實現了該接口,上代碼:

    public static void serializationAttack() throws Exception {
        // 對象序列化流去對對象進行操作
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("serFile"));
        //通過單例代碼獲取一個對象
        DoubleCheckLockSingleton s1 = DoubleCheckLockSingleton.getInstance();
        //將單例對象,通過序列化流,序列化到文件中
        outputStream.writeObject(s1);
        // 通過序列化流,將文件中序列化的對象信息讀取到內存中 
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("serFile")));
        //通過序列化流,去創建對象 
        DoubleCheckLockSingleton s2 = (DoubleCheckLockSingleton) inputStream.readObject();
        s1.tellEveryone();
        s2.tellEveryone();
        System.out.println(s1 == s2);
    }

執行結果如下:
This is a DoubleCheckLockSingleton 777874839
This is a DoubleCheckLockSingleton 254413710
false
爲什麼會發生這種事?長話短說,在 ObjectInputStream.readObject() 方法執行時,其內部方法
readOrdinaryObject()中有這樣一句話:

// desc是類描述符
obj = desc.isInstantiable() ? desc.newInstance() : null

也就是說,如果一個實現了Serializable/Externalizable接口的類可以在運行時實例化,那麼就調用
newInstance()方法,使用其默認構造方法反射創建新的對象實例,自然也就破壞了單例性。要防禦序
列化攻擊,就得將instance聲明爲transient,並且在單例中加入以下語句:

private Object readResolve() {
    return instance;
}

這是因爲在上述readOrdinaryObject()方法中,會通過衛語句 desc.hasReadResolveMethod() 檢查類
中是否存在名爲readResolve()的方法,如果有,就執行 desc.invokeReadResolve(obj) 調用該方
法。readResolve()會用自定義的反序列化邏輯覆蓋默認實現,因此強制它返回instance本身,就可以防
止產生新的實例。

枚舉單例的防禦機制

對反射的防禦

我們直接將上述reflectionAttack()方法中的雙重檢查鎖方式的類名改成EnumSingleton並執行,會拋出NoSuchMethodException。
這是因爲所有Java枚舉都隱式繼承自Enum抽象類,而Enum抽象類根本沒有無參構造方法,只有如下一
個構造方法:
那麼我們就改成獲取這個有參構造方法,即:
Constructor constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
結果還是會拋出異常
來到Constructor.newInstance()方法中,有如下語句:
可見,JDK反射機制內部完全禁止了用反射創建枚舉實例的可能性。

對序列化的防禦

如果將serializationAttack()方法中的攻擊目標換成EnumSingleton,那麼我們就會發現s1和s2實際上是
同一個實例,最終會打印出true。這是因爲ObjectInputStream類中,對枚舉類型有一個專門的
readEnum()方法來處理,其簡要流程如下:

  1. 通過類描述符取得枚舉單例類型
  2. 取得枚舉單例中的枚舉值的名字(這裏是INSTANCE)
  3. 調用Enum.valueOf()方法,根據枚舉類型和枚舉值的名字,獲得最終的單例
    這種處理方法與readResolve()方法大同小異,都是以繞過反射直接獲取單例爲目標。不同的是,枚舉對
    序列化的防禦仍然是JDK內部實現的。
    綜上所述,枚舉單例確實是目前最好的單例實現了,不僅寫法非常簡單,並且JDK能夠保證其安全性,
    不需要我們做額外的工作
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章