您好,登錄后才能下訂單哦!
本文小編為大家詳細介紹“Java單例模式與破壞單例模式的概念是什么”,內容詳細,步驟清晰,細節處理妥當,希望這篇“Java單例模式與破壞單例模式的概念是什么”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學習新知識吧。
經典設計模式又分23種,也就是GoF 23 總體分為三大類:
創建型模式
結構性模式
行為型模式
Java中單例模式是一種常見的設計模式,單例模式的寫法有好幾種,這里主要介紹三種:懶漢式單例、餓漢式單例、登記式單例。
單例模式有以下特點:
單例類只能有一個實例。
單例類必須自己創建自己的唯一實例。
單例類必須給所有其他對象提供這一實例。
單例模式確保某個類只有一個實例,而且自行實例化并向整個系統提供這個實例。在計算機系統中,線程池、緩存、日志對象、對話框、打印機、顯卡的驅動程序對象常被設計成單例。這些應用都或多或少具有資源管理器的功能。每臺計算機可以有若干個打印機,但只能有一個Printer Spooler,以避免兩個打印作業同時輸出到打印機中。每臺計算機可以有若干通信端口,系統應當集中管理這些通信端口,以避免一個通信端口同時被兩個請求同時調用。總之,選擇單例模式就是為了避免不一致狀態。
餓漢式單例: 在類加載時,就會創建好將會使用的對象,可能會造成內存的浪費
示例:
public class Hungry { // 創建唯一實例 private final static Hungry HUNGRY = new Hungry(); private Hungry(){} // 全局訪問點 ---> 拿到HUNGRY實例 public static Hungry getIntance(){ return HUNGRY; } }
而預加載就是先一步加載,我們沒有使用該單例對象但是已經將其加載到內存中,那么就會造成內存的浪費
懶漢式改善了餓漢式浪費內存的問題,等到需要用到實例的時候再去加載到內存中
懶漢式寫法( 線程不安全 ):
public class LazyMan { private LazyMan(){} public static LazyMan lazyMan; public static LazyMan getInstance(){ if (lazyMan==null){ lazyMan = new LazyMan(); } return lazyMan; } }
但是在不進行任何同步干預的情況下,懶漢式不是線程安全的單例模式,經典的解決方案就是利用雙重檢驗鎖保證程序的原子性和有序性,如下示例:
public class LazyMan { private LazyMan(){} // 懶漢當中的雙重檢驗鎖 --> 可以保證線程安全 public volatile static LazyMan lazyMan; // volatile 保證了new實例時不會發生指令重排 public static LazyMan getInstance(){ if (lazyMan==null){ synchronized (LazyMan.class){ // 此處上鎖 以保證原子操作 if (lazyMan == null){ lazyMan = new LazyMan(); } } } return lazyMan; } }
反射是一種動態獲取類資源的一種途徑,我們讓然可以通過反射來獲取單例模式中的更多實例:
public class LazyMan { // 空參構造器 private LazyMan(){} // 懶漢當中的雙重檢驗鎖 --> 可以保證線程安全 public volatile static LazyMan lazyMan; // volatile 保證了new實例時不會發生指令重排 public static LazyMan getInstance(){ if (lazyMan==null){ synchronized (LazyMan.class){ // 此處上鎖 以保證原子操作 if (lazyMan == null){ lazyMan = new LazyMan();// 不是原子操作 } } } return lazyMan; } public static void main(String[] args) throws Exception{ // 獲取無參構造器 Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null); constructor.setAccessible(true);// 無視私有 // 獲取實例 LazyMan instance2 = constructor.newInstance(); LazyMan instance3 = constructor.newInstance(); LazyMan instance4 = constructor.newInstance(); // 懶漢式單例 獲取唯一實例 LazyMan instance = LazyMan.getInstance(); System.out.println("getIntance獲取實例(1)hashCode:"+instance.hashCode()); System.out.println("反射構造器newIntance獲取實例(2)hashCode:"+instance2.hashCode()); System.out.println("反射構造器newIntance獲取實例(3)hashCode:"+instance3.hashCode()); System.out.println("反射構造器newIntance獲取實例(4)hashCode:"+instance4.hashCode()); } }
上述程序輸出結果如下:
/*
getIntance獲取實例(1)hashCode:895328852
反射構造器newIntance獲取實例(2)hashCode:1304836502
反射構造器newIntance獲取實例(3)hashCode:225534817
反射構造器newIntance獲取實例(4)hashCode:1878246837
*/
修復方式1:
// 對空參構造器進行上鎖 并對唯一實例lazyman判斷是否已經初始化 private LazyMan(){ if (lazyMan != null){ throw new RuntimeException("不要試圖破壞單例模式"); } }
但是這種修復方式仍然會被破壞,我們首先是利用了反射來獲取LazyMan的空參構造器,并利用其構造器進行初始化獲取實例,但是如果我們一直不調用getIntance方法來初始化lazyman實例而一直用反射獲取,那么這種方式就形同虛設
因此,得出下一個修復方式。我們依然對空參構造器進行上鎖,然后利用標志位保證我們的空參構造器只能使用一次,也就是最多只能為一個實例進行初始化。
修復方式2:
// 解決2. 對空參構造器進行上鎖 利用標志位保證空參構造器只能初始化一次實例 但是標志位字段仍可以通過其他途徑被拿到 并且修改 private static boolean flag = false; private LazyMan(){ synchronized(LazyMan.class){ if (flag == false){ flag = true; }else { throw new RuntimeException("不要試圖破壞單例模式"); } } }
上述代碼所示,利用flag作為標志位來保證空參構造器只能對最多一個實例執行初始化操作。但是,同時我們所設置的標志位flag同樣存在被通過各種渠道拿到的風險,比如反編譯。拿到flag標志后就可以對其修改,示例:
public static void main(String[] args) throws Exception{ // 獲取無參構造器 Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null); constructor.setAccessible(true);// 無視私有 // 懶漢式單例 獲取唯一實例 LazyMan instance = LazyMan.getInstance(); // (1) // 獲取標志位字段并進行修改 Field flag1 = LazyMan.class.getDeclaredField("flag"); // (1) 處已經調用了空參構造器 flag變為true 此處修改為false 可以繼續創建實例 flag1.set(instance,false); LazyMan instance2 = constructor.newInstance(); // 與上述同理 flag1.set(instance2,false); LazyMan instance3 = constructor.newInstance(); System.out.println("getIntance獲取實例(1)hashCode:"+instance.hashCode()); System.out.println("反射構造器newIntance獲取實例(2)hashCode:"+instance2.hashCode()); System.out.println("反射構造器newIntance獲取實例(3)hashCode:"+instance3.hashCode()); }
那么既然如此,是不是單例程序無論如何設計最終都會被反射破壞呢?
事實并非如此,我們打開反射得到的構造器.newInstance方法源碼查看:
// 我們只看如下兩行 if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects");
如上述代碼所示,Java給出的解釋為:
如果實際參數和形式參數的數量不同;如果原始參數的展開轉換失敗;或者如果在可能展開之后,參數值不能通過方法調用轉換轉換為相應的形式參數類型;如果此構造函數屬于枚舉類型。符合上述任一情況將會拋出IllegalArgumentException("Cannot reflectively create enum objects")
非法參數異常
也就是說,枚舉類型是可以避免單例模式被破壞的
public enum enumSingle { INSTANCE; public enumSingle getInstance() { return INSTANCE; } } class TestEnumSingle{ public static void main(String[] args) throws Exception { // 下面我們嘗試用反射來破壞枚舉類 // 枚舉類的構造器實際上帶有兩個參數 String和int Constructor<enumSingle> declaredConstructor = enumSingle.class.getDeclaredConstructor(String.class,int.class); declaredConstructor.setAccessible(true); // 直接獲取實例 enumSingle instance = enumSingle.INSTANCE; // 反射獲取實例 enumSingle enumSingle1 = declaredConstructor.newInstance(); System.out.println("類名直接訪問獲取實例hashCode:"+instance.hashCode()); System.out.println("反射實例hashCode:"+enumSingle1.hashCode()); } } // 最終拋出 java.lang.IllegalArgumentException: Cannot reflectively create enum objects
除了反射會打破單例之外,序列化Serializable
也同樣會破壞單例模式,具體體現是物品們同一對象在序列化前和反序列化之后不是同一對象。
讀到這里,這篇“Java單例模式與破壞單例模式的概念是什么”文章已經介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領會,如果想了解更多相關內容的文章,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。