最近學習了一下單例模式,看bilibili up主「狂神說Java」講完後,發現大部分博客都少了一個頗有趣的環節,不分享出來實在是太惋惜了,原視頻 https://www.bilibili.com/video/BV1K54y197iShtml
這個部分小部分我相信不少博客都講的很好,我就儘可能精簡了
總結:我認爲建立方法能夠歸根於兩種,一種是餓漢式,我在類的加載的時候就建立;還有一種懶漢式,只有在我須要的時候纔去建立
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。
【建議】建議使用靜態內部類實現
在面試官面前裝逼的時候來了
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 } }
【反射能破化枚舉的單例嗎?】
咱們最終能夠發現反射不能破化枚舉的單例
這種實現方式尚未被普遍採用,但這是實現單例模式的最佳方法。它更簡潔,自動支持序列化機制,絕對防止屢次實例化。(菜鳥教程官方)
【總結】太難了