Java單例模式與破壞單例模式概念原理深入講解
什么是單例模式
經(jīng)典設(shè)計(jì)模式又分23種,也就是GoF 23 總體分為三大類(lèi):
- 創(chuàng)建型模式
- 結(jié)構(gòu)性模式
- 行為型模式
Java中單例模式是一種常見(jiàn)的設(shè)計(jì)模式,單例模式的寫(xiě)法有好幾種,這里主要介紹三種:懶漢式單例、餓漢式單例、登記式單例。
單例模式有以下特點(diǎn):
- 單例類(lèi)只能有一個(gè)實(shí)例。
- 單例類(lèi)必須自己創(chuàng)建自己的唯一實(shí)例。
- 單例類(lèi)必須給所有其他對(duì)象提供這一實(shí)例。
單例模式確保某個(gè)類(lèi)只有一個(gè)實(shí)例,而且自行實(shí)例化并向整個(gè)系統(tǒng)提供這個(gè)實(shí)例。在計(jì)算機(jī)系統(tǒng)中,線程池、緩存、日志對(duì)象、對(duì)話框、打印機(jī)、顯卡的驅(qū)動(dòng)程序?qū)ο蟪1辉O(shè)計(jì)成單例。這些應(yīng)用都或多或少具有資源管理器的功能。每臺(tái)計(jì)算機(jī)可以有若干個(gè)打印機(jī),但只能有一個(gè)Printer Spooler,以避免兩個(gè)打印作業(yè)同時(shí)輸出到打印機(jī)中。每臺(tái)計(jì)算機(jī)可以有若干通信端口,系統(tǒng)應(yīng)當(dāng)集中管理這些通信端口,以避免一個(gè)通信端口同時(shí)被兩個(gè)請(qǐng)求同時(shí)調(diào)用??傊?,選擇單例模式就是為了避免不一致?tīng)顟B(tài)。
餓漢式(預(yù)加載)
餓漢式單例: 在類(lèi)加載時(shí),就會(huì)創(chuàng)建好將會(huì)使用的對(duì)象,可能會(huì)造成內(nèi)存的浪費(fèi)
示例:
public class Hungry { // 創(chuàng)建唯一實(shí)例 private final static Hungry HUNGRY = new Hungry(); private Hungry(){} // 全局訪問(wèn)點(diǎn) ---> 拿到HUNGRY實(shí)例 public static Hungry getIntance(){ return HUNGRY; } }
而預(yù)加載就是先一步加載,我們沒(méi)有使用該單例對(duì)象但是已經(jīng)將其加載到內(nèi)存中,那么就會(huì)造成內(nèi)存的浪費(fèi)
懶漢式(懶加載)
懶漢式改善了餓漢式浪費(fèi)內(nèi)存的問(wèn)題,等到需要用到實(shí)例的時(shí)候再去加載到內(nèi)存中
懶漢式寫(xiě)法( 線程不安全 ):
public class LazyMan { private LazyMan(){} public static LazyMan lazyMan; public static LazyMan getInstance(){ if (lazyMan==null){ lazyMan = new LazyMan(); } return lazyMan; } }
但是在不進(jìn)行任何同步干預(yù)的情況下,懶漢式不是線程安全的單例模式,經(jīng)典的解決方案就是利用雙重檢驗(yàn)鎖保證程序的原子性和有序性,如下示例:
public class LazyMan { private LazyMan(){} // 懶漢當(dāng)中的雙重檢驗(yàn)鎖 --> 可以保證線程安全 public volatile static LazyMan lazyMan; // volatile 保證了new實(shí)例時(shí)不會(huì)發(fā)生指令重排 public static LazyMan getInstance(){ if (lazyMan==null){ synchronized (LazyMan.class){ // 此處上鎖 以保證原子操作 if (lazyMan == null){ lazyMan = new LazyMan(); } } } return lazyMan; } }
反射破壞單例模式
反射是一種動(dòng)態(tài)獲取類(lèi)資源的一種途徑,我們讓然可以通過(guò)反射來(lái)獲取單例模式中的更多實(shí)例:
public class LazyMan { // 空參構(gòu)造器 private LazyMan(){} // 懶漢當(dāng)中的雙重檢驗(yàn)鎖 --> 可以保證線程安全 public volatile static LazyMan lazyMan; // volatile 保證了new實(shí)例時(shí)不會(huì)發(fā)生指令重排 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{ // 獲取無(wú)參構(gòu)造器 Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null); constructor.setAccessible(true);// 無(wú)視私有 // 獲取實(shí)例 LazyMan instance2 = constructor.newInstance(); LazyMan instance3 = constructor.newInstance(); LazyMan instance4 = constructor.newInstance(); // 懶漢式單例 獲取唯一實(shí)例 LazyMan instance = LazyMan.getInstance(); System.out.println("getIntance獲取實(shí)例(1)hashCode:"+instance.hashCode()); System.out.println("反射構(gòu)造器newIntance獲取實(shí)例(2)hashCode:"+instance2.hashCode()); System.out.println("反射構(gòu)造器newIntance獲取實(shí)例(3)hashCode:"+instance3.hashCode()); System.out.println("反射構(gòu)造器newIntance獲取實(shí)例(4)hashCode:"+instance4.hashCode()); } }
上述程序輸出結(jié)果如下:
/*
getIntance獲取實(shí)例(1)hashCode:895328852
反射構(gòu)造器newIntance獲取實(shí)例(2)hashCode:1304836502
反射構(gòu)造器newIntance獲取實(shí)例(3)hashCode:225534817
反射構(gòu)造器newIntance獲取實(shí)例(4)hashCode:1878246837
*/
修復(fù)方式1:
// 對(duì)空參構(gòu)造器進(jìn)行上鎖 并對(duì)唯一實(shí)例lazyman判斷是否已經(jīng)初始化 private LazyMan(){ if (lazyMan != null){ throw new RuntimeException("不要試圖破壞單例模式"); } }
但是這種修復(fù)方式仍然會(huì)被破壞,我們首先是利用了反射來(lái)獲取LazyMan的空參構(gòu)造器,并利用其構(gòu)造器進(jìn)行初始化獲取實(shí)例,但是如果我們一直不調(diào)用getIntance方法來(lái)初始化lazyman實(shí)例而一直用反射獲取,那么這種方式就形同虛設(shè)
因此,得出下一個(gè)修復(fù)方式。我們依然對(duì)空參構(gòu)造器進(jìn)行上鎖,然后利用標(biāo)志位保證我們的空參構(gòu)造器只能使用一次,也就是最多只能為一個(gè)實(shí)例進(jìn)行初始化。
修復(fù)方式2:
// 解決2. 對(duì)空參構(gòu)造器進(jìn)行上鎖 利用標(biāo)志位保證空參構(gòu)造器只能初始化一次實(shí)例 但是標(biāo)志位字段仍可以通過(guò)其他途徑被拿到 并且修改 private static boolean flag = false; private LazyMan(){ synchronized(LazyMan.class){ if (flag == false){ flag = true; }else { throw new RuntimeException("不要試圖破壞單例模式"); } } }
上述代碼所示,利用flag作為標(biāo)志位來(lái)保證空參構(gòu)造器只能對(duì)最多一個(gè)實(shí)例執(zhí)行初始化操作。但是,同時(shí)我們所設(shè)置的標(biāo)志位flag同樣存在被通過(guò)各種渠道拿到的風(fēng)險(xiǎn),比如反編譯。拿到flag標(biāo)志后就可以對(duì)其修改,示例:
public static void main(String[] args) throws Exception{ // 獲取無(wú)參構(gòu)造器 Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null); constructor.setAccessible(true);// 無(wú)視私有 // 懶漢式單例 獲取唯一實(shí)例 LazyMan instance = LazyMan.getInstance(); // (1) // 獲取標(biāo)志位字段并進(jìn)行修改 Field flag1 = LazyMan.class.getDeclaredField("flag"); // (1) 處已經(jīng)調(diào)用了空參構(gòu)造器 flag變?yōu)閠rue 此處修改為false 可以繼續(xù)創(chuàng)建實(shí)例 flag1.set(instance,false); LazyMan instance2 = constructor.newInstance(); // 與上述同理 flag1.set(instance2,false); LazyMan instance3 = constructor.newInstance(); System.out.println("getIntance獲取實(shí)例(1)hashCode:"+instance.hashCode()); System.out.println("反射構(gòu)造器newIntance獲取實(shí)例(2)hashCode:"+instance2.hashCode()); System.out.println("反射構(gòu)造器newIntance獲取實(shí)例(3)hashCode:"+instance3.hashCode()); }
那么既然如此,是不是單例程序無(wú)論如何設(shè)計(jì)最終都會(huì)被反射破壞呢?
事實(shí)并非如此,我們打開(kāi)反射得到的構(gòu)造器.newInstance方法源碼查看:
// 我們只看如下兩行 if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects");
如上述代碼所示,Java給出的解釋為:
如果實(shí)際參數(shù)和形式參數(shù)的數(shù)量不同;如果原始參數(shù)的展開(kāi)轉(zhuǎn)換失?。换蛘呷绻诳赡苷归_(kāi)之后,參數(shù)值不能通過(guò)方法調(diào)用轉(zhuǎn)換轉(zhuǎn)換為相應(yīng)的形式參數(shù)類(lèi)型;如果此構(gòu)造函數(shù)屬于枚舉類(lèi)型。符合上述任一情況將會(huì)拋出IllegalArgumentException("Cannot reflectively create enum objects")
非法參數(shù)異常
也就是說(shuō),枚舉類(lèi)型是可以避免單例模式被破壞的
public enum enumSingle { INSTANCE; public enumSingle getInstance() { return INSTANCE; } } class TestEnumSingle{ public static void main(String[] args) throws Exception { // 下面我們嘗試用反射來(lái)破壞枚舉類(lèi) // 枚舉類(lèi)的構(gòu)造器實(shí)際上帶有兩個(gè)參數(shù) String和int Constructor<enumSingle> declaredConstructor = enumSingle.class.getDeclaredConstructor(String.class,int.class); declaredConstructor.setAccessible(true); // 直接獲取實(shí)例 enumSingle instance = enumSingle.INSTANCE; // 反射獲取實(shí)例 enumSingle enumSingle1 = declaredConstructor.newInstance(); System.out.println("類(lèi)名直接訪問(wèn)獲取實(shí)例hashCode:"+instance.hashCode()); System.out.println("反射實(shí)例hashCode:"+enumSingle1.hashCode()); } } // 最終拋出 java.lang.IllegalArgumentException: Cannot reflectively create enum objects
除了反射會(huì)打破單例之外,序列化Serializable
也同樣會(huì)破壞單例模式,具體體現(xiàn)是物品們同一對(duì)象在序列化前和反序列化之后不是同一對(duì)象
到此這篇關(guān)于Java單例模式與破壞單例模式概念原理深入講解的文章就介紹到這了,更多相關(guān)Java單例模式內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot中@RestControllerAdvice注解的使用
這篇文章主要介紹了SpringBoot中@RestControllerAdvice注解的使用,@RestControllerAdvice主要用精簡(jiǎn)客戶端返回異常,它可以捕獲各種異常,需要的朋友可以參考下2024-01-01java設(shè)計(jì)模式:建造者模式之生產(chǎn)線
這篇文章主要介紹了Java設(shè)計(jì)模式之建造者模式,結(jié)合具體實(shí)例形式分析了建造者模式的概念、原理、實(shí)現(xiàn)方法與相關(guān)使用注意事項(xiàng),需要的朋友可以參考下2021-08-08SpringBoot中@ConditionalOnBean實(shí)現(xiàn)原理解讀
這篇文章主要介紹了SpringBoot中@ConditionalOnBean實(shí)現(xiàn)原理,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02SpringMVC中@RequestMapping注解的實(shí)現(xiàn)
RequestMapping是一個(gè)用來(lái)處理請(qǐng)求地址映射的注解,本文主要介紹了SpringMVC中@RequestMapping注解的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2024-01-01Java類(lèi)加載器ClassLoader源碼層面分析講解
ClassLoader翻譯過(guò)來(lái)就是類(lèi)加載器,普通的java開(kāi)發(fā)者其實(shí)用到的不多,但對(duì)于某些框架開(kāi)發(fā)者來(lái)說(shuō)卻非常常見(jiàn)。理解ClassLoader的加載機(jī)制,也有利于我們編寫(xiě)出更高效的代碼。ClassLoader的具體作用就是將class文件加載到j(luò)vm虛擬機(jī)中去,程序就可以正確運(yùn)行了2022-09-09java基礎(chǔ)開(kāi)發(fā)泛型類(lèi)的詳解
這篇文章為大家介紹了java基礎(chǔ)開(kāi)發(fā)中泛型類(lèi)的詳解,包括泛型的概念以及應(yīng)用實(shí)例有需要的朋友可以借鑒參考下,希望能夠有所幫助2021-10-10springboot之SpringApplication生命周期和事件機(jī)制解讀
這篇文章主要介紹了springboot之SpringApplication生命周期和事件機(jī)制,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06Java Socket+mysql實(shí)現(xiàn)簡(jiǎn)易文件上傳器的代碼
最近在做一個(gè)小項(xiàng)目,項(xiàng)目主要需求是實(shí)現(xiàn)一個(gè)文件上傳器,通過(guò)客戶端的登陸,把本地文件上傳到服務(wù)器的數(shù)據(jù)庫(kù)(本地的)。下面通過(guò)本文給大家分享下實(shí)現(xiàn)代碼,感興趣的朋友一起看看吧2016-10-10