【设计模式】各个击破单例模式的8种写法
單例模式
在一個系統(tǒng)開發(fā)過程中,我們在基于節(jié)省內(nèi)存資源、保證數(shù)據(jù)內(nèi)容的一致性的考慮上,往往需要對某些類要求只能創(chuàng)建一個實例,即「保證類只有一個實例」的設(shè)計模式就是單例模式。
比如我們遇到過的各種Manager管理類,各種Factory工廠類;
Spring 框架應(yīng)用中的 ApplicationContext、數(shù)據(jù)庫中的連接池等也都是單例模式。
本文旨在淺析一下單例模式的寫法。
單例模式的8種寫法
1. 餓漢式
/***?類加載的時候就實例化一個實例,JVM保證線程安全*?也稱餓漢式*?該方式簡單實用*?@author?行百里er*/ public?class?Singleton_1?{private?static?final?Singleton_1?INSTANCE?=?new?Singleton_1();/***?私有的構(gòu)造方法,其他地方不能new*/private?Singleton_1()?{}public?static?Singleton_1?getInstance()?{return?INSTANCE;}/***?for?test*/public?static?void?main(String[]?args)?{Singleton_1?instance1?=?Singleton_1.getInstance();Singleton_1?instance2?=?Singleton_1.getInstance();System.out.println(instance1?==?instance2);} }這是我推薦使用的方法,簡單實用,寫著安心,用著放心。
2. 意思同第一種,只是寫法不同
/***?和Singleton_1是同一個意思*?只是將new的動作放在了代碼塊里*?類加載到內(nèi)存的時候只加載一個實例*?@author?行百里er*/ public?class?Singleton_2?{private?static?final?Singleton_2?INSTANCE;static?{INSTANCE?=?new?Singleton_2();}/***?私有的構(gòu)造方法,其他地方不能new*/private?Singleton_2()?{}public?static?Singleton_2?getInstance()?{return?INSTANCE;}/***?for?test*/public?static?void?main(String[]?args)?{Singleton_2?instance1?=?Singleton_2.getInstance();Singleton_2?instance2?=?Singleton_2.getInstance();System.out.println(instance1?==?instance2);} }和第一種寫法是同一個意思,只是將new的動作放在了代碼塊里,類加載到內(nèi)存的時候只加載一個實例。
3. 懶漢式,用到的時候才實例化
/***?懶漢式*?這種寫法雖然達(dá)到了用的時候才初始化的目的,但是存在多線程獲取實例時相互影響的問題*?@author?行百里er*/ public?class?Singleton_3?{private?static?Singleton_3?INSTANCE;/***?私有的構(gòu)造方法,其他地方不能new*/private?Singleton_3()?{}public?static?Singleton_3?getInstance()?{if?(INSTANCE?==?null)?{//?sleep一下,測試try?{Thread.sleep(1);}?catch?(InterruptedException?e)?{e.printStackTrace();}INSTANCE?=?new?Singleton_3();}return?INSTANCE;}/***?for?test*/public?static?void?main(String[]?args)?{//同一個類的不同對象的hashcode不同//跑100個線程,看看有沒有不同的實例for?(int?i?=?0;?i?<?100;?i++)?{new?Thread(()?->?System.out.println(Singleton_3.getInstance().hashCode())).start();}} }這種寫法雖然達(dá)到了用的時候才初始化的目的,但是「存在多線程獲取實例時相互影響」的問題。
運行結(jié)果:
4. 在上一種寫法的基礎(chǔ)上加synchronized鎖,保證線程安全
/***?lazy?loading?懶漢式*?*?可以用synchronized加鎖,但是效率會降低*?@author?行百里er*/ public?class?Singleton_4?{private?static?Singleton_4?INSTANCE;/***?私有的構(gòu)造方法,其他地方不能new*/private?Singleton_4()?{}/***?既然lazy?loading的寫法有線程安全問題,那就加把鎖*/public?static?synchronized?Singleton_4?getInstance()?{if?(INSTANCE?==?null)?{//?測試,sleep一下,增加被其他線程打斷的機會try?{Thread.sleep(1);}?catch?(InterruptedException?e)?{e.printStackTrace();}INSTANCE?=?new?Singleton_4();}return?INSTANCE;}/***?for?test*/public?static?void?main(String[]?args)?{//同一個類的不同對象的hashcode不同//跑100個線程,看看有沒有不同的實例for?(int?i?=?0;?i?<?100;?i++)?{new?Thread(()?->?System.out.println(Singleton_4.getInstance().hashCode())).start();}} }這種方式可行,但會帶來效率下降的問題。下面繼續(xù)“吹毛求疵”。
5. 那就在線程安全的基礎(chǔ)上減少鎖住的代碼數(shù)量,「這里有坑」
/***?lazy?loading?懶漢式*?在加鎖的基礎(chǔ)上再優(yōu)化一下,減少加鎖代碼塊的數(shù)量*?@author?行百里er*/ public?class?Singleton_5?{private?static?Singleton_5?INSTANCE;/***?私有的構(gòu)造方法,其他地方不能new*/private?Singleton_5()?{}public?static?Singleton_5?getInstance()?{if?(INSTANCE?==?null)?{//不在方法上加鎖而在new的時候才加鎖,減少鎖的代碼,然而這種方式并不行synchronized?(Singleton_5.class)?{//?測試,sleep一下,增加被其他線程打斷的機會try?{Thread.sleep(1);}?catch?(InterruptedException?e)?{e.printStackTrace();}INSTANCE?=?new?Singleton_5();}}return?INSTANCE;}/***?for?test*/public?static?void?main(String[]?args)?{//同一個類的不同對象的hashcode不同//跑100個線程,看看有沒有不同的實例for?(int?i?=?0;?i?<?100;?i++)?{new?Thread(()?->?System.out.println(Singleton_5.getInstance().hashCode())).start();}} }先看運行結(jié)果:
說明這種寫法看似在線程安全有減少了鎖的代碼量,其實是達(dá)不到“永遠(yuǎn)”單例的目的的。
「原因分析」:線程A運行完if(INSTANCE == null),還沒拿到鎖時候,線程B也運行到if(INSTANCE == null)這一句并且拿到鎖進(jìn)行了new實例化,然后線程B釋放鎖,線程A得到鎖繼續(xù)運行if語句塊里面的內(nèi)容進(jìn)行new的過程,這樣就出現(xiàn)了不同的實例了。
6. 那就來個雙重檢查鎖(Double Check Locking)吧
/***?在加鎖的基礎(chǔ)上再優(yōu)化一下,減少加鎖代碼塊的數(shù)量---事實證明不可行*?那就雙重檢查DCL*?@author?行百里er*/ public?class?Singleton_6?{private?static?Singleton_6?INSTANCE;/***?私有的構(gòu)造方法,其他地方不能new*/private?Singleton_6()?{}public?static?Singleton_6?getInstance()?{if?(INSTANCE?==?null)?{?//第一重檢查synchronized?(Singleton_6.class)?{if?(INSTANCE?==?null)?{?//第二重檢查//?測試,sleep一下,增加被其他線程打斷的機會try?{Thread.sleep(1);}?catch?(InterruptedException?e)?{e.printStackTrace();}INSTANCE?=?new?Singleton_6();}}}return?INSTANCE;}/***?for?test*/public?static?void?main(String[]?args)?{//同一個類的不同對象的hashcode不同//跑100個線程,看看有沒有不同的實例for?(int?i?=?0;?i?<?100;?i++)?{new?Thread(()?->?System.out.println(Singleton_6.getInstance().hashCode())).start();}} }這里,提個問題:「INSTANCE要不要加volatile」?
答案是肯定的。
「volatile」的作用是「保證線程可見性和禁止指令重排序」。在DCL單利模式寫法中,volatile主要是用于禁止指令重排序的。因為如果不加volatile關(guān)鍵字,那么可能會出現(xiàn)指令重排序。
假設(shè):一個線程A執(zhí)行到 「INSTANCE = new Singleton_6()」 的時候,經(jīng)過編譯器編譯,會分成三個指令(注意 INSTANCE 是static的):
給指令申請內(nèi)存
給成員變量初始化
把這塊內(nèi)存的內(nèi)容賦值給INSTANCE
既然有值了,那么線程B上來先進(jìn)行檢查發(fā)現(xiàn)已經(jīng)有值,就不會進(jìn)入加鎖那部分的代碼了。
加了「volatile」后,就不允許指令重排序了。所以此時一定是保證線程A初始化完了才會復(fù)制給這個變量。
7. 靜態(tài)內(nèi)部類方式
/***?靜態(tài)內(nèi)部類方式*?JVM保證單例*?加載外部類時不會加載內(nèi)部類---也可實現(xiàn)懶加載*?@author?行百里er*/ public?class?Singleton_7?{/***?私有的構(gòu)造方法,其他地方不能new*/private?Singleton_7()?{}private?static?class?SingletonHolder?{private?static?final?Singleton_7?INSTANCE?=?new?Singleton_7();}public?static?Singleton_7?getInstance()?{return?SingletonHolder.INSTANCE;}/***?for?test*/public?static?void?main(String[]?args)?{//同一個類的不同對象的hashcode不同//跑100個線程,看看有沒有不同的實例for?(int?i?=?0;?i?<?100;?i++)?{new?Thread(()?->?System.out.println(Singleton_7.getInstance().hashCode())).start();}} }即可保證單例(虛擬機保證),也能實現(xiàn)懶加載。
如果非要追求完美,那么可以用這種方式。
8. 完美中的完美方式,Enum實現(xiàn)單例
/***?枚舉單例*?不僅可以解決線程同步,還可以防止反序列化*/ public?enum?Singleton_8?{INSTANCE;/***?for?test*/public?static?void?main(String[]?args)?{//同一個類的不同對象的hashcode不同//跑100個線程,看看有沒有不同的實例for?(int?i?=?0;?i?<?100;?i++)?{new?Thread(()?->?System.out.println(Singleton_8.INSTANCE.hashCode())).start();}} }膜拜一波這種寫法!這是Java創(chuàng)始人之一的大神在《Effective Java》這本書中推薦的寫法。
小結(jié)
雖然單例模式有這么多種寫法,但不少是炫技式的花活,有點像孔乙己的“茴”字的N中寫法。
這里我們理解其中有些寫法的“瑕疵”和其中蘊含的“原理”就可以了。
有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號
好文章,我在看??
總結(jié)
以上是生活随笔為你收集整理的【设计模式】各个击破单例模式的8种写法的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring Boot 集成 Elast
- 下一篇: JavaWeb——通过Listener理