1. 程式人生 > >java設計模式精講 Debug 方式+記憶體分析 第8章 單例模式

java設計模式精講 Debug 方式+記憶體分析 第8章 單例模式

單例模式

8-1 單例模式講解

在這裡插入圖片描述


在這裡插入圖片描述


在這裡插入圖片描述


在這裡插入圖片描述


在這裡插入圖片描述


在這裡插入圖片描述


在這裡插入圖片描述


在這裡插入圖片描述


在這裡插入圖片描述


在這裡插入圖片描述


8-2 單例設計模式-懶漢式及多執行緒Debug實戰

懶漢式單例:

public class LazySingleton {
    /** 懶漢模式的話,開始沒有進行初始化 */
    private static LazySingleton lazySingleton = null;
    /** 構造器要進行私有化 */
    private LazySingleton
(){ } public static LazySingleton getInstance() { /** 這個是有執行緒安全的問題 */ if (lazySingleton == null) { /** * 如果一個執行緒進來了,但是這個時候,在new例項的時候,阻塞了或者還沒有new出例項, * 這個時候,另外一個執行緒判斷lazySingleton依然是空的,那麼就這時候,也進來了, * 那麼這個時候,就是有執行緒安全問題的 */
lazySingleton = new LazySingleton(); } return lazySingleton; } }

我們來測試一下:

public class Test {
    public static void main(String[]args){
        LazySingleton lazySingleton = LazySingleton.getInstance();
        System.out.println(lazySingleton);
    }
}

debug除錯:
在這裡插入圖片描述


我們再來看看多執行緒的時候,會出現什麼問題:

public class T implements Runnable{
    @Override
    public void run() {
        LazySingleton lazySingleton = LazySingleton.getInstance();
        System.out.println(Thread.currentThread().getName()+" "+lazySingleton);
    }
}

測試程式碼如下:

public class Test {
    public static void main(String[]args){
        Thread t1 = new Thread(new T());
        Thread t2 = new Thread(new T());
        t1.start();
        t2.start();
        System.out.println("program end");
    }
}

program end
Thread-1 [email protected]46
Thread-0 [email protected]46


這個時候,我們就要用到多執行緒debug來進行除錯:
在這裡插入圖片描述


模擬兩個執行緒,一個執行緒在進入if之後,還沒有new出例項, 這個時候,另外一個執行緒也進來了,if判斷這個時候還沒有例項,於是也進入了if裡面,這個時候,就new出來兩個例項:
在這裡插入圖片描述


在這裡插入圖片描述


在這裡插入圖片描述


然後,我們在切換到thread0,讓它賦值上:
在這裡插入圖片描述


接著我們切換到Thread1:
在這裡插入圖片描述


在這裡插入圖片描述


在這裡插入圖片描述


我們接著向下執行:
Thread0和Thread1都放過:
雖然此時,兩個物件還是同一個物件,但是是經過了修改了的:
在這裡插入圖片描述


我們再debug返回不同的物件:
我們先都兩個執行緒進入if判斷,然後一個執行緒直接執行完成,再另外一個執行緒執行完成,這個時候,返回的就是兩個物件了:
在這裡插入圖片描述


我們對懶漢式單例模式的執行緒安全問題有幾種解決方案:
方式一:在獲取例項的方法上新增synchronized關鍵字
如果鎖載入靜態方法上 ,那麼就相當於鎖是加在這個類的class檔案;如果不是靜態方法相當於是在堆記憶體中生成的物件:

public class LazySingleton {
    /** 懶漢模式的話,開始沒有進行初始化 */
    private static LazySingleton lazySingleton = null;
    /** 構造器要進行私有化 */
    private LazySingleton(){

    }
    public synchronized static LazySingleton getInstance() {
        if (lazySingleton == null) {
            lazySingleton =  new LazySingleton();
        }
        return lazySingleton;
    }
}

方式二:把獲取例項的方法內新增synchronized程式碼塊

public class LazySingleton {
    /** 懶漢模式的話,開始沒有進行初始化 */
    private static LazySingleton lazySingleton = null;
    /** 構造器要進行私有化 */
    private LazySingleton(){

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

在這裡插入圖片描述


我們再執行Thread1,發現執行不下去了,已經是阻塞的狀態了:
在這裡插入圖片描述


最後拿的就是同一個物件:
在這裡插入圖片描述
但是,我們知道,加鎖和解鎖的時候,是會帶來額外的開銷,對效能會有一定的影響;我們再來進行演進,在效能和安全上進行平衡;


8-3 單例設計模式-DoubleCheck雙重檢查實戰及原理解析

我們 可以這樣來寫:

public class LazyDoubleCheckSingleton {
    /** 懶漢模式的話,開始沒有進行初始化 */
    private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    /** 構造器要進行私有化 */
    private LazyDoubleCheckSingleton(){

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

在這裡插入圖片描述


這個時候,會有一個風險:那就是發生了重排序:

public class LazyDoubleCheckSingleton {
    /** 懶漢模式的話,開始沒有進行初始化 */
    private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    /** 構造器要進行私有化 */
    private LazyDoubleCheckSingleton(){

    }
    public static LazyDoubleCheckSingleton getInstance() {
        /** 如果2和3進行重排序,那麼這裡的判斷並不為空,這個時候,實際上物件還沒有初始化好,就可以進行這個判斷 */
        if (lazyDoubleCheckSingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazyDoubleCheckSingleton == null) {
                    /**
                     * 實際上有三個步驟:
                     * 1. 分配記憶體給這個物件
                     * 2. 初始化物件
                     * 3.設定lazyDoubleCheckSingleton指向剛分配的記憶體地址
                     * 2和3的順序有可能會被顛倒,
                     *
                     * 這個時候,就規定所有的執行緒在執行java程式的時候,必須要遵守intra-thread semantics這麼一個規定
                     * 它保證了重排序不會改變單執行緒內的程式執行結果
                     */
                    lazyDoubleCheckSingleton =  new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

下圖就是表示單執行緒的情況:規定所有的執行緒在執行java程式的時候,必須要遵守intra-thread semantics這麼一個規定,它保證了重排序不會改變單執行緒內的程式執行結果。
在這裡插入圖片描述


現在來看一下多執行緒的情況:
在這裡插入圖片描述


我們可以嘗試不允許重排序:
我們在初始化的時候,給它加上一個volatile關鍵字:這個時候,就可以實現執行緒安全的延遲初始化,這樣的話,重排序就是會被禁止,在多執行緒的時候,CPU也有共享記憶體,我們加上了這個關鍵字了之後,所有執行緒就能看到共享記憶體的最新狀態,保證了記憶體的可見性,使用volatile的時候,在進行寫操作的時候,會多出一些彙編程式碼,起到兩個作用1)將當前處理器快取好的資料寫回到系統記憶體中,其他記憶體從共享記憶體中同步資料,這樣的話,就保證了共享記憶體的可見性,這裡就是使用了快取一致性的協議,當發現快取記憶體中的資料無效,會重新從系統記憶體中把資料讀回處理器的記憶體裡;

public class LazyDoubleCheckSingleton {
    /** 懶漢模式的話,開始沒有進行初始化 */
    private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    /** 構造器要進行私有化 */
    private LazyDoubleCheckSingleton(){

    }
    public static LazyDoubleCheckSingleton getInstance() {
        /** 如果2和3進行重排序,那麼這裡的判斷並不為空,這個時候,實際上物件還沒有初始化好,就可以進行這個判斷 */
        if (lazyDoubleCheckSingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazyDoubleCheckSingleton == null) {
                    /**
                     * 實際上有三個步驟:
                     * 1. 分配記憶體給這個物件
                     * 2. 初始化物件
                     * 3.設定lazyDoubleCheckSingleton指向剛分配的記憶體地址
                     * 2和3的順序有可能會被顛倒,
                     *
                     * 這個時候,就規定所有的執行緒在執行java程式的時候,必須要遵守intra-thread semantics這麼一個規定
                     * 它保證了重排序不會改變單執行緒內的程式執行結果
                     */
                    lazyDoubleCheckSingleton =  new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

通過volatile和doubleCheck的這種方式既兼顧了效能,又兼顧了執行緒安全的問題;


我們來進行測試一下:
在這裡插入圖片描述


這個時候,拿到這個就是同一個物件:
在這裡插入圖片描述

8-4 單例設計模式-靜態內部類-基於類初始化的延遲載入解決方案及原理解析


這個就是用靜態內部類來實現單例模式:

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

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

    }
}

在這裡插入圖片描述


在這裡插入圖片描述

8-5 單例設計模式-餓漢式

在類載入的時候,就完成了例項化,避免了執行緒同步的問題,缺點就是在類載入的時候,就完成了初始化,沒有延遲載入,這個時候,就是會造成記憶體的浪費

public class HungrySingleton {
    private final static HungrySingleton hungrySingleton = new HungrySingleton();
    private HungrySingleton() {

    }
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

也可以這樣來寫:

public class HungrySingleton {
    private final static HungrySingleton hungrySingleton;
    static {
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton() {

    }
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

餓漢式和懶漢式最大的區別就是在有沒有延遲載入;


8-6 單例設計模式-序列化破壞單例模式原理解析及解決方案

我們給這類實現一個序列化介面:

public class HungrySingleton implements Serializable {
    private final static HungrySingleton hungrySingleton;
    static {
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton() {

    }
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

我們來測試:

public class Test {
    public static void main(String[]args) throws IOException, ClassNotFoundException {
        HungrySingleton instance = HungrySingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
        oos.writeObject(instance);

        File file = new File("singleton_file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        HungrySingleton newInstance = (HungrySingleton) ois.readObject();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

輸出結果:

[email protected]1dae
[email protected]7951
false


這個時候,通過序列化和反序列化拿了不同的物件;而我們希望拿到的是同一的物件,我們可以這樣來做:

public class HungrySingleton implements Serializable {
    private final static HungrySingleton hungrySingleton;
    static {
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton() {

    }
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
    /** 我們加上這樣的一個方法 */
    private Object readResolve() {
        return hungrySingleton;
    }
}

我們再來進行測試,這個時候,兩個物件就是同一個物件:

[email protected]1dae
[email protected]1dae
true


我們通過用debug的方式檢視原始碼,我們可以看出這個方法是通過反射出來的:
在這裡插入圖片描述


在這裡插入圖片描述


在這裡插入圖片描述


在這裡插入圖片描述


在這裡插入圖片描述
一旦,我們在程式中使用了序列化的時候,一定要考慮序列化對單例破壞;


8-7 單例設計模式-反射攻擊解決方案及原理分析


我們用反射來寫:

public class Test {
    public static void main(String[]args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class objectClass = HungrySingleton.class;
        Constructor constructor = objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        HungrySingleton instance = HungrySingleton.getInstance();
        HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance==newInstance);
    }
}

測試結果:

[email protected]cd21
[email protected]5dca
false


現在,我們就來寫反射防禦的程式碼:
在這裡插入圖片描述
在用靜態內部類來生成的也可以用上這個反射防禦的方式:

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

    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.staticInnerClassSingleton;
    }
    private StaticInnerClassSingleton(){
        if (InnerClass.staticInnerClassSingleton != null) {
            throw new RuntimeException("單例構造器禁止反射呼叫");
        }
    }
}

8-8 單例設計模式-Enum列舉單例、原理原始碼解析以及反編譯實戰


public enum  EnumInstance {
    INSTANCE;
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
    public static EnumInstance getInstance() {
        return INSTANCE;
    }
}

在這裡插入圖片描述


我們要呼叫方法的話,那麼我們就是可以這樣來寫:

public enum  EnumInstance {
    INSTANCE {
        @Override
        protected void printTest() {
            System.out.println("Geely Print Test");
        }
    };
    protected abstract void printTest();

    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
    public static EnumInstance getInstance() {
        return INSTANCE;
    }
}

8-9 單例設計模式-容器單例

我們可以這樣來寫:

public class ContainerSingleton {
    private static Map<String, Object> singletonMap = new HashMap<>();
    public static void putInstance(String key,Object instance) {
        if (StringUtils.isNotBlank(key) && instance!=null) {
            if