大家好,我是課代表。歡迎關(guān)注我的公眾號(hào)_Java課代表,原創(chuàng)實(shí)戰(zhàn)干貨首發(fā)地兒,不要錯(cuò)過(guò)呦!
來(lái),我們開(kāi)始今天的分享!
0.前言
如果你去問(wèn)一個(gè)寫(xiě)過(guò)幾年代碼的程序員用過(guò)哪些設(shè)計(jì)模式,我打賭,90_以上的回答里面會(huì)帶【單例模式】。甚至有的面試官會(huì)直接問(wèn)_說(shuō)一下你用過(guò)哪些設(shè)計(jì)模式,單例就不用說(shuō)了。你看,連面試官都聽(tīng)煩了,火爆程度可見(jiàn)一斑。
不過(guò),看似簡(jiǎn)單的單例模式,里面蘊(yùn)含了很多Java基礎(chǔ),日常開(kāi)發(fā)過(guò)程中課代表見(jiàn)過(guò)很多不規(guī)范的,甚至是有問(wèn)題的單例實(shí)現(xiàn)。所以整理此文,總結(jié)一下單例模式的最佳實(shí)踐。
1、懶加載(懶漢)
所謂懶加載,就是直到第一次被調(diào)用時(shí)才加載。其實(shí)現(xiàn)需要考慮并發(fā)問(wèn)題和指令重排,代碼如下_
public class Singleton { private volatile static Singleton instance; //① private Singleton() { //② } public static Singleton getInstance() { if (instance __ null) {//③ synchronized (Singleton.class) { if (instance __ null) {//④ instance _ new Singleton();//⑤ } } } return instance; }}
這段代碼精簡(jiǎn)至極,沒(méi)有一個(gè)字符是多余的,下面逐行解讀一下_
首先,注意到①處的volatile關(guān)鍵字,她具備兩項(xiàng)特性_
一是保證此變量對(duì)于所有線程的可見(jiàn)性。即當(dāng)一條線程修改了這個(gè)變量的值,新值對(duì)于其他線程來(lái)說(shuō)是可以立即得知的。
二是禁止指令重排序優(yōu)化。
這里解釋一下指令重排序優(yōu)化_
代碼 ⑤ 處的instance _ new Singleton();并不是原子的,大體可分為如下 3 步_
- 分配內(nèi)存調(diào)用構(gòu)造函數(shù)初始化成實(shí)例
- 讓instance指向分配的內(nèi)存空間
JVM 允許在保證結(jié)果正確的前提下進(jìn)行指令重排序優(yōu)化。即如上 3 步可能的順序?yàn)?->2->3 或 1->3->2 。如果順序是 1->3->2 ,當(dāng) 3 執(zhí)行完,2 還未執(zhí)行時(shí),另一個(gè)線程執(zhí)行到代碼 ③ 處,發(fā)現(xiàn)instance不為null,直接返回還未初始化好的instance并使用,就會(huì)報(bào)錯(cuò)。
所以使用volatile,就是為了保證線程間的可見(jiàn)性和防止指令重排。
其次,代碼②處將構(gòu)造函數(shù)聲明為private目的在于阻止使用new Singleton()這樣的代碼生成新實(shí)例。
最后,當(dāng)客戶端調(diào)用Singleton.getInstance()時(shí),先檢查是否已經(jīng)實(shí)例化(代碼③),未實(shí)例化時(shí)同步代碼塊,然后再次檢查是否已實(shí)例化(代碼④),然后才執(zhí)行代碼⑤。兩次檢查的意義在于,防止synchronized同步過(guò)程中其他線程進(jìn)行了實(shí)例化。
這就是著名的雙重檢查鎖(Double check lock)實(shí)現(xiàn)單例,也即懶加載。
TIPS:
網(wǎng)上也有直接對(duì)getInstance()方法加鎖的版本,這樣大范圍的方法級(jí)別加鎖會(huì)導(dǎo)致并發(fā)變低,實(shí)際上第一次調(diào)用生成實(shí)例之后,后續(xù)獲取實(shí)例根本不需要并發(fā)控制了。而本例的雙重檢查鎖版本可以避免此并發(fā)問(wèn)題。
2、預(yù)加載(餓漢)
與懶加載相對(duì)應(yīng),預(yù)加載是在類加載時(shí)就已經(jīng)初始化好了,所以是天然線程安全的,代碼如下_
public class Singleton { private static final Singleton instance _ new Singleton();// ① private Singleton(){} public static Singleton getInstance(){ return instance; }}
注意到 ① 處的類變量使用了final。
這里用final更多的意義在于提供語(yǔ)法約束。畢竟你是單例,就只有這一個(gè)實(shí)例,不可能再指向另一個(gè)。instance有了final的約束,后面再有人不小心編寫(xiě)了修改其指向的代碼就會(huì)報(bào)語(yǔ)法錯(cuò)誤。
這就好比@Override注解,你能保證寫(xiě)對(duì)方法名和參數(shù),那不寫(xiě)注解也沒(méi)問(wèn)題,但是有了注解的約束,編譯器就會(huì)幫你檢查,還能防止別人亂改。
3、靜態(tài)內(nèi)部類
此方法和預(yù)加載原理相同,都是利用JVM類加載的特性實(shí)現(xiàn)天然的線程安全,不同之處在于,靜態(tài)內(nèi)部類做到了延遲加載。
public class Singleton { private static class SingletonHolder { private static Singleton instance _ new Singleton(); } private Singleton(){} public static Singleton getInstance() { return SingletonHolder.instance; }}
SingletonHolder 是靜態(tài)內(nèi)部類,當(dāng)外部類Singleton被加載的時(shí)候并不會(huì)創(chuàng)建任何實(shí)例,只有當(dāng)Singleton.getInstance()被調(diào)用的時(shí)候,才會(huì)創(chuàng)建Singleton實(shí)例,這一切由 JVM 天然完成,所以既保證了線程安全,又實(shí)現(xiàn)了延遲加載。
4、枚舉
沒(méi)錯(cuò),枚舉可以實(shí)現(xiàn)單例,而且這種方式是《Effective Java中文版》第二版 中的推薦實(shí)現(xiàn)方式。代碼極其簡(jiǎn)單_
public enum Singleton { INSTANCE; public void doSomeThing(){ System.out.println("done"); }}
使用時(shí)直接Singleton.INSTANCE.doSomeThing();即可。
這里主要利用了枚舉的如下兩個(gè)特性_
枚舉的構(gòu)造器總是私有的,所以不必像前幾種方式一樣顯式定義私有構(gòu)造方法除此之外,枚舉還附帶了一些額外好處_無(wú)償?shù)靥峁┝诵蛄谢瘷C(jī)制,還可以防止通過(guò)多次反序列化生成多個(gè)實(shí)例。
鑒于此,單例的最佳實(shí)踐就是用枚舉來(lái)實(shí)現(xiàn)。
5、總結(jié)
事實(shí)上,單例的寫(xiě)法并不止于本文所提的這 4 種,你可能還會(huì)看到很多其他變種,她們或多或少都存在一些缺陷,比如,懶加載方式將synchronized作用于整個(gè)方法上也能實(shí)現(xiàn),但頻繁加鎖,釋放鎖會(huì)產(chǎn)生性能瓶頸,而完全去掉鎖又會(huì)帶來(lái)并發(fā)問(wèn)題。
所以,只要吃透了文中列出的這 4 種單例方式,就能做到舉一反三,見(jiàn)到別人寫(xiě)的單例也能一眼看出對(duì)錯(cuò)。
文中所列的 4 種單例模式,除了枚舉之外,全都用到了static關(guān)鍵字,《Java 虛擬機(jī)規(guī)范》 規(guī)定,有幾種情況必須立即對(duì)類進(jìn)行“初始化”,其中涉及static的場(chǎng)景如下_
讀取或設(shè)置一個(gè)類型的靜態(tài)字段(被 final 修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候。
調(diào)用一個(gè)類型的靜態(tài)方法的時(shí)候。
懶加載,預(yù)加載和靜態(tài)內(nèi)部類正是利用了這兩點(diǎn)特性。
對(duì)static關(guān)鍵字遺忘的同學(xué)可以參看我的另一篇文章_《一題搞定static關(guān)鍵字》
最后,再次強(qiáng)調(diào)一下,如果大家開(kāi)發(fā)中需要手寫(xiě)單例,建議聽(tīng)從 Joshua Bloch在《Effective Java中文版》第二版 中的建議_
單元素的枚舉類型已經(jīng)成為實(shí)現(xiàn) Singleton 的最佳方法
參考資料_
1、《Effective Java中文版》 Joshua Bloch 第二版 P15
2、《深入理解 Java 虛擬機(jī)》 周志明 第3版,P444-P448,P264
3、深入淺出單實(shí)例SINGLETON設(shè)計(jì)模式
相關(guān)原創(chuàng)推薦
一題搞定static關(guān)鍵字