單例模式-23種設計模式系列

什麼是單例模式

單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。這種類型的設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。
這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。
注意:
1、單例類只能有一個實例。
2、單例類必須自己創建自己的唯一實例。
3、單例類必須給所有其他對象提供這一實例。

餓漢式單例模式


//餓漢式單例
public class Hungry {
    //由於初始化的時候類裏面的內容會全部加載,可能造成內存浪費!!
    private byte[] data1=new byte[1024*1024];
    private byte[] data2=new byte[1024*1024];
    private byte[] data3=new byte[1024*1024];
    private byte[] data4=new byte[1024*1024];

    private Hungry(){
    }

    private final static Hungry HUNGRY=new Hungry();

    private static Hungry getInstance(){
        return HUNGRY;
    }

}

懶漢式單例模式

//懶漢式單例
public class LazyMan {

    private LazyMan(){
    }

    //先不初始化
    private static LazyMan LAZY_MAN;

    //只有爲空的時候初始化
    private static LazyMan getInstance(){
        if (LAZY_MAN==null){
            LAZY_MAN=new LazyMan();
        }
        return LAZY_MAN;
    }
}

這樣的單利在單線程下是可以的,但是在多線程時就會失效。

測試用例

public static void main(String[] args) {
        for (int i = 0; i <10 ; i++) {
            new Thread(()->{
                LazyMan.getInstance();
            }).start();
        }
    }

在這裏插入圖片描述

DCL懶漢式單例模式

由於在多線程條件下,上面的懶漢式不安全,所以我們可以加一層鎖,把class鎖住,這樣就只有一個class了。這就是雙重檢測鎖懶漢式,又叫DCL懶漢式。

//雙重檢測鎖
    private static LazyMan getInstance(){
        if (LAZY_MAN==null){//第一層檢測
            synchronized (LazyMan.class){//鎖
                if (LAZY_MAN==null){//第二層檢測
                    LAZY_MAN=new LazyMan();
                }
            }
        }
        return LAZY_MAN;
    }

在這裏插入圖片描述

volatile

上面這樣就真的安全了嗎?

LAZY_MAN=new LazyMan();

我們知道new一個對象不算原子性操作,是由三步組成的。
1、分配內存空間 2、執行構造方法,初始化對象 3、把這個對象指向分配的空間。
正常情況,CPU是按123的順序執行的,但是也有可能是132的順序。如果線程A的執行順序是132,在執行完13後,線程B開啓,判斷LAZY_MAN不爲已經存在,但是指向的空間是沒有初始化的,就出現了問題。
所以,我們最好是在定義LAZY_MAN時加上volatile關鍵字。

private volatile static LazyMan LAZY_MAN;

靜態內部類實現單例模式

除了以上的基本方式,我們還可以使用靜態內部類實現單例模式,但是也不安全,不推薦使用。(可以用於裝B和炫技)


//靜態內部類實現
public class Holder {
    private Holder(){
    }

    private static Holder getInstance(){
        return InnerHolder.HOLDER;
    }
    //寫一個內部類創造HOLDER
    private static class InnerHolder{
        private final static Holder HOLDER=new Holder();
    }
}

反射破解DCL單例

只要有反射,任何代碼都是不安全的。
DCL單例在多線程下也是安全的,不會被破壞,但是我們還是要問一句
== 這樣就真的安全了嗎 ==

//反射破解DCL單例
    public static void main(String[] args) throws Exception{
        LazyMan instance = LazyMan.getInstance();
        Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        LazyMan instance2 = constructor.newInstance();
        System.out.println(instance);
        System.out.println(instance2);
    }

在這裏插入圖片描述
那遇到這種情況,我們又該怎麼解決呢?
由於反射是通過得到我們構造器來創造對象,所以我們可以在構造器裏面加一個鎖!確定只有一個構造器被初始化。

private LazyMan(){
        synchronized (LazyMan.class){
            if (LAZY_MAN!=null)
                throw  new RuntimeException("不要試圖破壞單例!!!");
        }
    }

在這裏插入圖片描述
這樣就真的安全了嗎?
上面我們是new了一個lazyman的對象,所以在構造器可以檢測到,但是如果兩個對象都是用反射創建的,這樣的單例還安全嗎?

//反射破解DCL單例
    public static void main(String[] args) throws Exception{
        //LazyMan instance = LazyMan.getInstance();
        Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        LazyMan instance = constructor.newInstance();
        LazyMan instance2 = constructor.newInstance();
        System.out.println(instance);
        System.out.println(instance2);
    }

在這裏插入圖片描述
遇到這種情況又該如何解決呢?
我們可以額外的設置一個變量,設置一個初始值,如何在構造器裏改變這個值,如果這個值改變了,就說明已經執行過了,就拋出異常。

//定義一個隨機變量,變量名一般比較複雜,我這裏用“櫻島麻衣”
    private static boolean yingdaomayi=false;

    private LazyMan(){
        if (!yingdaomayi)
            yingdaomayi=true;
        else {
            throw  new RuntimeException("不要試圖破壞單例!!!");
        }
    }

在這裏插入圖片描述

這樣就真的安全了嗎?

如果有人對你的源碼進行了反編譯,得到了這個變量名(無論你設置的在複雜,都會有破解的辦法),不就可以在創建完第一個對象後,再把這個變量設置爲初始化,這樣單例還安全嗎?

//反射破解DCL單例
    public static void main(String[] args) throws Exception{
        //LazyMan instance = LazyMan.getInstance();
        Field yingdaomayi = LazyMan.class.getDeclaredField("yingdaomayi");
        yingdaomayi.setAccessible(true);
        Constructor<LazyMan> constructor =LazyMan.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        LazyMan instance = constructor.newInstance();
        yingdaomayi.set(instance,false);
        LazyMan instance2 = constructor.newInstance();
        System.out.println(instance);
        System.out.println(instance2);
    }

在這裏插入圖片描述

遇到這種情況又要怎麼解決呢?

我們進入instance源碼可知,可知枚舉是不能用反射破壞的。所以我們要在好好地認識一下枚舉!

在這裏插入圖片描述

枚舉

我們先創建一個enum測試一下她的單例是不是真的不能被反射破壞。

//enum本身也是一個class類
public enum  EnumSingle {
    ENUM_SINGLE;
    private EnumSingle getInstance(){
        return ENUM_SINGLE;
    }
}

class Test {
    public static void main(String[] args) {
        EnumSingle enumSingle = EnumSingle.ENUM_SINGLE;
        EnumSingle enumSingle2 = EnumSingle.ENUM_SINGLE;
        System.out.println(enumSingle);
        System.out.println(enumSingle2);
    }
}

在這裏插入圖片描述
我們運行後看一下target下面的class文件,發現生成的構造器是無參的,然後我們可以用反射測試一下。
在這裏插入圖片描述

public static void main(String[] args) throws Exception {
        EnumSingle enumSingle = EnumSingle.ENUM_SINGLE;
        //EnumSingle enumSingle2 = EnumSingle.ENUM_SINGLE;
        Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        EnumSingle enumSingle2 = constructor.newInstance();
        System.out.println(enumSingle);
        System.out.println(enumSingle2);
    }

但是運行測試後出現了問題,找不到無參構造器。
在這裏插入圖片描述

運行結果是不會騙人的,所以只能是idea騙了我。

經過反編譯工具,我們看到了真正的源碼

在這裏插入圖片描述

發現並不是無參構造器,而是有參構造,類型是string和int。所以我們修改了我們的測試代碼

public static void main(String[] args) throws Exception {
        EnumSingle enumSingle = EnumSingle.ENUM_SINGLE;
        //EnumSingle enumSingle2 = EnumSingle.ENUM_SINGLE;
        Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        constructor.setAccessible(true);
        EnumSingle enumSingle2 = constructor.newInstance();
        System.out.println(enumSingle);
        System.out.println(enumSingle2);
    }

在這裏插入圖片描述

終於出現了應該的結果,枚舉果然不能被反射破壞單例。

但是枚舉真的不能被破解嗎?

序列化也是可以破解的,這裏就不深究了其實我也不會

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