深入探討單例模式

yxming發表於2020-04-17

最近學習了一下單例模式,看bilibili up主“狂神說Java”講完後,發現大部分部落格都少了一個很有趣的環節,不分享出來實在是太可惜了,原視訊 https://www.bilibili.com/video/BV1K54y197iS

1、瞭解單例

這個部分小部分我相信很多部落格都講的很好,我就儘量精簡了
  1. 注意:
  • 單例類只能有一個例項
  • 這個例項由自己建立
  • 這個例項必須提供給外界
  1. 關鍵:構造器私有化
  2. 建立方法:
  • 餓漢式
  • 懶漢式

總結:我認為建立方法可以歸根於兩種,一種是餓漢式,我在類的載入的時候就建立;還有一種懶漢式,只有在我需要的時候才去建立

2、思路及實現

【餓漢模式最基本的實現】

在類載入的時候就已經建立了,這個模式下,執行緒是安全的,不同的執行緒拿到的都是同一個例項,但是,這個也存在空間浪費的問題,我不需要的時候你也載入了。

//餓漢模式
 public class HungerSingle {
    private static HungerSingle single = new HungerSingle();
    //構造器私有,外界不能通過構造方法new物件,保證唯一
    private HungerSingle() {
    }
    //提供外界獲得該單例的方法,注意方法只能是static方法,因為沒有類例項
    public static HungerSingle getInstance(){
        return single;
    }
}

【懶漢模式最基本的實現】

為了解決上述那個空間浪費問題,這時候懶漢模式就起作用了,你需要我的時候我再去建立這個例項

//懶漢模式
public class LazySingle {
    private static LazySingle single;
    //構造器私有化,禁止外部new生成物件
    private LazySingle(){
    }
    //外界獲得該單例的方法
    public static LazySingle getInstance(){
        if(single == null){
            single = new LazySingle();
        }
        return single;
    }
 }

一位熱心前輩的評論:“像你這樣寫單例,在我們公司是要被開除的。”
趁我還是學生,懷著以後不被開除的心情,繼續學習下去
原來懶漢模式下,單例執行緒是不安全的。

怎麼測試呢?如下

【測試懶漢模式執行緒不安全】

//1、構造器
private LazySingle(){
    System.out.println(Thread.currentThread().getName());
}

//建立十個執行緒
for (int i = 0; i < 10; i++) {
    new Thread(()->{
         Singleton2.getInstance();
    }).start();
}

此時你會發現,構造方法呼叫了不止一次,說明沒有實現預期的單例

平時我們解決執行緒不安全的方法:不就是執行緒不安全嘛,那好辦,加鎖

【雙重檢測鎖/DCL】

public class DCLSingle {
    private static DCLSingle single;
    private DCLSingle(){
    }
    public static DCLSingle getInstance(){
        //第一次判斷,沒有這個物件才加鎖
        if(single == null){
            //哪個需要保護,就鎖哪個
            synchronized (DCLSingle.class){
                //第二次判斷,沒有就例項化
                if(single == null){
                    single = new DCLSingle();
                }
            }
        }
        return single;
    }

}

仔細和別人程式碼一比對,發現我少了個volatile關鍵字,這是啥玩意?
不懂就問。

【volatile】
為了避免指令重排

//上述程式碼宣告上面加上volatile關鍵字
 private volatile static DCLSingle single;

啥是volatile ?

引用自別人部落格
https://www.cnblogs.com/YLsY/p/11295732.html

加volatile是為了出現髒讀的出現,保證操作的原子性

1、原子性操作:不可再分割的操作
例如:single = new DCLSingle();
其實就是兩步操作:
①new DCLSingle();//開闢堆記憶體
②singl指向對記憶體

2、髒讀
Java記憶體模型規定所有的變數都是存在主存當中,每個執行緒都有自己的工作記憶體。
執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接對主存進行操作。
並且每個執行緒不能訪問其他執行緒的工作記憶體。
變數的值何時從執行緒的工作記憶體寫回主存,無法確定。

3、指令重排
single = new DCLSingle();
先執行②
後執行①
//先指向堆記憶體,還未完成構造


【模擬情況】
①執行緒1執行,在自己的工作記憶體定義引用,先指向堆記憶體,還未構造完成
②此時執行緒2執行,它進行判斷,引用已經指向了記憶體,所以執行緒2,認為構造完成,實際還未構造完成

還有一種差點忘記說了,也是菜鳥教程說建議使用的方式

【靜態內部類實現單例】

public class Singleton {
    private Singleton(){}
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton getInstance(){
        return SingletonHolder .INSTANCE;
    }
}

你會發現它和前面講的普通餓漢式很像,我把它也歸於餓漢式一類,因為它也是直接就new Singleton,但是它卻有著懶載入的效果,而這種方式是 Singleton 類被裝載了,INSTANCE不一定被初始化。因為 SingletonHolder 類沒有被主動使用,只有通過顯式呼叫 getInstance 方法時,才會顯式裝載 SingletonHolder 類,從而例項化 INSTANCE。

【建議】建議使用靜態內部類實現


3、如何破化單例(其它大部分部落格沒有的內容)

在這裡感謝b站up【狂神說java】

在面試官面前裝逼的時候來了

java語言實現動態化的靈魂——反射,說:沒有什麼是我不能改變的,看我來如何操作。

【反射破壞單例】

public class DCLSingle {
    private static DCLSingle single;
    private DCLSingle(){
    }
    public static DCLSingle getInstance(){
        //第一次判斷,沒有這個物件才加鎖
        if(single == null){
            //哪個需要保護,就鎖哪個
            synchronized (DCLSingle.class){
                //第二次判斷,沒有就例項化
                if(single == null){
                    single = new DCLSingle();
                }
            }
        }
        return single;
    }
    
    //通過反射破化單例
    public static void main(String[] args) throws Exception {
        LazySingle single = LazySingle.getInstance();
        Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        LazySingle single1 = constructor.newInstance();
        System.out.println(single == single1);//false
    }

}

得到單例類的構造器,然後通過newInstance的方法建立物件,很明顯破化了單例

【改進程式碼,防止你搞破化】

既然這次你是通過得到構造器破化的,那我給構造器加個方法,如果你已經建立了例項,那就丟擲異常

private LazySingle(){
    synchronized(LazySingle.class){
        if(single!=null){
            throw new RuntimeException("破壞失敗");
        }
    }
}

但是這個又有問題,這裡的判斷是private static DCLSingle single 是否有值,如果我們都不通過getInstance()方法建立物件,而是這樣

public static void main(String[] args) throws Exception {
 //   LazySingle single = LazySingle.getInstance();
    Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    
    //注意:這裡的物件不是單例類中裡面屬性的那個物件
    LazySingle single = constructor.newInstance();
    LazySingle single1 = constructor.newInstance();
    System.out.println(single == single1);//false
}

這裡根本不會丟擲異常,而是又破壞了單例

【繼續改進程式碼,防止搞破化】
簡直就是相愛相殺呀,我們可以利用紅路燈原理,防止破化
改進構造方法

//加個標誌
private static String sign = "password";
private LazySingle(){
    synchronized(LazySingle.class){
        if(single!=null || !"password".equals(sign)){
            throw new RuntimeException("破壞失敗");
        }else{
            sign = "no";
        }
    }
    
}

此刻你通過上述main()方法裡面的內容測試,發現又會丟擲異常。然而我們能通過反射獲得構造方法,那我們同樣也能通過反射獲取物件的屬性以及值吧

【再度破化】

public static void main(String[] args) throws Exception {
    Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    Field field = LazySingle.class.getDeclaredField("sign");
    //此處省略通過反射獲取該屬性的型別和方法....
    LazySingle single1 = constructor.newInstance();
    //重新變回原標誌位
    field.set("sign","password");
    LazySingle single2 = constructor.newInstance();
    System.out.println(single2 == single1);//false
}

又被破化了

【再次改進】

我們將目光拋向列舉,
jdk1.5之後,出現列舉
利用列舉實現不僅能避免多執行緒同步問題,而且還自動支援序列化機制,防止反序列化重新建立新的物件,絕對防止多次例項化(菜鳥教程官方術語)

public enum Singleton {  
    INSTANCE;  
    public Singleton getInstance() {  
        return INSTANCE
    }  
}

【反射能破化列舉的單例嗎?】

  1. 我們先要了解列舉是啥,它的底層是怎麼實現的
  2. 我們會發現列舉本身就是一個類
  3. 通過反編譯工具,檢視列舉底層的構造方法
  4. 通過反射獲取構造方法
  5. 重複上述反射測試

我們最終可以發現反射不能破化列舉的單例

這種實現方式還沒有被廣泛採用,但這是實現單例模式的最佳方法。它更簡潔,自動支援序列化機制,絕對防止多次例項化。(菜鳥教程官方)

【總結】太難了

相關文章