深刻探討單例模式

最近學習了一下單例模式,看bilibili up主「狂神說Java」講完後,發現大部分博客都少了一個頗有趣的環節,不分享出來實在是太惋惜了,原視頻 https://www.bilibili.com/video/BV1K54y197iShtml

一、瞭解單例

這個部分小部分我相信不少博客都講的很好,我就儘可能精簡了
  1. 注意:
  • 單例類只能有一個實例
  • 這個實例由本身建立
  • 這個實例必須提供給外界
  1. 關鍵:構造器私有化
  2. 建立方法:
  • 餓漢式
  • 懶漢式

總結:我認爲建立方法能夠歸根於兩種,一種是餓漢式,我在類的加載的時候就建立;還有一種懶漢式,只有在我須要的時候纔去建立

java

二、思路及實現

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

在類加載的時候就已經建立了,這個模式下,線程是安全的,不一樣的線程拿到的都是同一個實例,可是,這個也存在空間浪費的問題,我不須要的時候你也加載了。安全

//餓漢模式
 public class HungerSingle {
    private static HungerSingle single = new HungerSingle();
    //構造器私有,外界不能經過構造方法new對象,保證惟一
    private HungerSingle() {
    }
    //提供外界得到該單例的方法,注意方法只能是static方法,由於沒有類實例
    public static HungerSingle getInstance(){
        return single;
    }
}

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

爲了解決上述那個空間浪費問題,這時候懶漢模式就起做用了,你須要個人時候我再去建立這個實例ide

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

一位熱心前輩的評論:「像你這樣寫單例,在咱們公司是要被開除的。」
趁我仍是學生,懷着之後不被開除的心情,繼續學習下去
原來懶漢模式下,單例線程是不安全的。工具

怎麼測試呢?以下學習

【測試懶漢模式線程不安全】測試

//一、構造器
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 ?

引用自別人博客
http://www.javashuo.com/article/p-bfigxqiv-bc.html

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

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

二、髒讀
Java內存模型規定全部的變量都是存在主存當中,每一個線程都有本身的工做內存。
線程對變量的全部操做都必須在工做內存中進行,而不能直接對主存進行操做。
而且每一個線程不能訪問其餘線程的工做內存。
變量的值什麼時候從線程的工做內存寫回主存,沒法肯定。

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


【模擬狀況】
①線程1執行,在本身的工做內存定義引用,先指向堆內存,還未構造完成
②此時線程2執行,它進行判斷,引用已經指向了內存,因此線程2,認爲構造完成,實際還未構造完成

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

【靜態內部類實現單例】

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

你會發現它和前面講的普通餓漢式很像,我把它也歸於餓漢式一類,由於它也是直接就new Singleton,可是它卻有着懶加載的效果,而這種方式是 Singleton 類被裝載了,instance 不必定被初始化。由於 SingletonHolder 類沒有被主動使用,只有經過顯式調用 getInstance 方法時,纔會顯式裝載 SingletonHolder 類,從而實例化 instance。

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


## 三、如何破化單例(其它大部分博客沒有的內容) 在這裏感謝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. 重複上述測試

咱們最終能夠發現反射不能破化枚舉的單例

這種實現方式尚未被普遍採用,但這是實現單例模式的最佳方法。它更簡潔,自動支持序列化機制,絕對防止屢次實例化。(菜鳥教程官方)

【總結】太難了

相關文章
相關標籤/搜索