設計模式(七): 單例模式(懶漢、餓漢、靜態內部類、雙重檢驗鎖、枚舉、序列化反序列化、反射攻擊、容器單例)

1. 定義

在這裏插入圖片描述
優點
在這裏插入圖片描述
缺點
在這裏插入圖片描述
特性:
(1)私有構造函數
(2)線程安全
(3)延遲加載
(4)序列化和反序列化
(5)反射攻擊

2. 懶漢模式

在這裏插入圖片描述
多線程創建:
在這裏插入圖片描述
主函數直接調用
在這裏插入圖片描述
開啓線程調試:類型設置爲Thread
在這裏插入圖片描述
開始調試:
在這裏插入圖片描述
thread0
在這裏插入圖片描述
thread1
在這裏插入圖片描述
直接往下走,生成兩個對象。
在這裏插入圖片描述

3. 雙重檢驗鎖

public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazySingleton = null;

    private LazyDoubleCheckSingleton(){

    }

    public static LazyDoubleCheckSingleton getInstance(){
        if(lazySingleton==null){//1.第一次檢查,此處是爲了減少性能開銷,不用每次進入synchronized中
            synchronized (LazyDoubleCheckSingleton.class) {
                if(lazySingleton==null){//2.第二次檢測

                    /*問題:原來的排序過程是:1、2、3,但是可能jvm指令重排序成爲1、3、2,
                    * 此時可能兩個線程都在執行2,執行3的時候這兩個線程都new了LazyDoubleCheckSingleton對象
                    *
                    * 解決辦法:volatile
                    * */
                    //1.分配內存給這個對象
                    //3.設置lazyDoubleCheckSingleton 指向剛分配的內存地址
                    //2.new一個LazyDoubleCheckSingleton對象
                    lazySingleton = new LazyDoubleCheckSingleton();
                }
            }
        }

        return lazySingleton;
    }

}

4. 靜態內部類

public class StaticInnerClassSingleton {
    private static class InnerClass{
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }
    public static StaticInnerClassSingleton getInstance(){
        return InnerClass.staticInnerClassSingleton;
    }

    private StaticInnerClassSingleton(){
    }
}

靜態內部類在外部類被調用時不會立即被初始化,只有當其中的靜態內部參數被調用時,纔會被初始化。這樣保證懶加載。
通過靜態內部類的初始化staticInnerClassSingleton,jvm保證類的初始化只能有一個線程可以同時初始化同一個類,也就是獲取初始化InnerClass靜態類的鎖只會被一個線程獲取到,所以在InnerClass被初始化時,其中的靜態資源會優先被初始化。

相比雙重檢驗鎖:畢竟volatile禁止了指令重排,所以雙重檢驗鎖的效率肯定略低一點。

5.懶漢模式

public class HungrySingleton implements Serializable {
    private static HungrySingleton ourInstance = new HungrySingleton();

    public static HungrySingleton getInstance() {
        return ourInstance;
    }

    private HungrySingleton() {
    }
}

特點:在類加載的時候完成對單例類的初始化。

6.序列化對單例的破壞

在這裏插入圖片描述
流程
(1)ObjectInputStream中readObject讀取一個object
(2)ObjectInputStream中readObject0讀取
(3)readObject0方法中發現是一個object調用readOrdinaryObject

case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));

(4)readOrdinaryObject中有,表示只要是實現了序列化就new一個對象,否則返回null

obj = desc.isInstantiable() ? desc.newInstance() : null;

(5)調用了cons.newInstance()

@CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

(6)Reflection.getCallerClass()調用反射實現了new一個新的對象。

故前後不是同一個對象。

重寫:
(1)在readOrdinaryObject中反射完成一個對象的創建以後會調用desc.invokeReadResolve(obj)
(2)desc.invokeReadResolve(obj)中return readResolveMethod.invoke(obj, (Object[]) null);
(3) readResolveMethod.invoke調用原方法的readResolve方法,於是可以通過重寫readResolve實現返回原來的單例對象。

在這裏插入圖片描述

7. 反射攻擊單例

反射修改私有構造器的權限來new一個新的單例對象。
在這裏插入圖片描述
防止反射攻擊
在這裏插入圖片描述
懶漢式在多線程條件下無法防止反射在這裏插入圖片描述
無法防止的原因:先利用構造器new了一個單例對象,然後getInstance()方法new了一個對象。
在這裏插入圖片描述

8. 枚舉完成單例

枚舉類型
在這裏插入圖片描述
反序列化無法攻擊枚舉
在這裏插入圖片描述
在ObjectInputStream中readEnum方法去讀取數據時,會從常量池中去尋找這些數據。所以枚舉中的data無論何時被獲取都是相等。

反射攻擊枚舉:報錯,獲取構造器失敗

查看源碼發現,枚舉沒有空構造器,必須要帶上參數

在這裏插入圖片描述
傳入參數後重試
在這裏插入圖片描述
查看源碼:發現構造器無法獲取枚舉對象
在這裏插入圖片描述

繼續使用jad EnumInstance.class生成EnumInstance.jad,裏面有如下代碼:

public final class EnumInstance extends Enum // final類
{

    public static EnumInstance[] values()
    {
        return (EnumInstance[])$VALUES.clone();
    }

    public static EnumInstance valueOf(String name)
    {
        return (EnumInstance)Enum.valueOf(com/qianliu/creational/songleton/EnumInstance, name);
    }

    private EnumInstance(String s, int i)//私有構造器
    {
        super(s, i);
    }

    public static EnumInstance getInstance()
    {
        return INSTANCE;
    }

    public String getData()
    {
        return data;
    }

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

    public static final EnumInstance INSTANCE;
    String data;
    private static final EnumInstance $VALUES[];

    static 
    {
        INSTANCE = new EnumInstance("INSTANCE", 0);//在靜態模塊初始化單例對象
        $VALUES = (new EnumInstance[] {
            INSTANCE
        });
    }
}

Enum類似於餓漢模式聲明對象,類加載時完成初始化,再加上反射和io相關的類保證枚舉類型的單例。

使用枚舉類內部的方法:枚舉類內部的方法必須顯式聲明。
在這裏插入圖片描述

9. 容器實現單例

如果我們使用的過程中單例對象特別多,可以用這種方式實現單例
在這裏插入圖片描述

10. ThreadLocal實現特殊的單例

在這裏插入圖片描述
ThreadLocal可以保證每個線程內部的對象是單例的,不同線程直接按互不干擾
在這裏插入圖片描述

源碼:https://github.com/LUK-qianliu/design_pattern_in_all

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章