單例模式是保證一個(gè)類的實(shí)例有且只有一個(gè),在需要控制資源(如數(shù)據(jù)庫連接池),或資源共享(如有狀態(tài)的工具類)的場景中比較適用。如果讓我們寫一個(gè)單例實(shí)現(xiàn),估計(jì)絕大部分人都覺得自己沒問題,但如果需要實(shí)現(xiàn)一個(gè)比較完美的單例,可能并沒有你想象中簡單。本文以主人公小雨的一次面試為背景,循序漸進(jìn)地討論如何實(shí)現(xiàn)一個(gè)較為“完美”的單例。本文人物與場景皆為虛構(gòu),如有雷同,純屬捏造。
小雨計(jì)算機(jī)專業(yè)畢業(yè)三年,對(duì)設(shè)計(jì)模式略有涉獵,能寫一些簡單的實(shí)現(xiàn),掌握一些基本的JVM知識(shí)。在某次面試中,面試官要求現(xiàn)場寫代碼:請(qǐng)寫一個(gè)你認(rèn)為比較“完美”的單例。
簡單的單例實(shí)現(xiàn)
憑借著對(duì)單例的理解與印象,小雨寫出了下面的代碼
public class Singleton { private static Singleton instance; private
Singleton(){} public static final Singleton getInstance(){ if(instance == null)
{ instance = new Singleton(); } return instance; } }
寫完后小雨審視了一遍,總覺得有點(diǎn)太簡單了,離“完美”貌似還相差甚遠(yuǎn)。對(duì),在多線程并發(fā)環(huán)境下,這個(gè)實(shí)現(xiàn)就玩不轉(zhuǎn)了,如果兩個(gè)線程同時(shí)調(diào)用
getInstance() 方法,同時(shí)執(zhí)行到了 if 判斷,則兩邊都認(rèn)為 instance 實(shí)例為空,都會(huì)實(shí)例化一個(gè) Singleton
對(duì)象,就會(huì)導(dǎo)致至少產(chǎn)生兩個(gè)實(shí)例了,小雨心想。嗯,需要解決多線程并發(fā)環(huán)境下的同步問題,保證單例的線程安全。
線程安全的單例
一提到并發(fā)同步問題,小雨就想到了鎖。加個(gè)鎖還不簡單,synchronized 搞起,
public class Singleton { private static Singleton instance; private
Singleton(){} public synchronized static final Singleton getInstance(){
if(instance == null) { instance = new Singleton(); } return instance; } }
小雨再次審視了一遍,發(fā)現(xiàn)貌似每次 getInstance()
被調(diào)用時(shí),其它線程必須等待這個(gè)線程調(diào)用完才能執(zhí)行(因?yàn)橛墟i鎖住了嘛),但是加鎖其實(shí)是想避免多個(gè)線程同時(shí)執(zhí)行實(shí)例化操作導(dǎo)致產(chǎn)生多個(gè)實(shí)例,在單例被實(shí)例化后,后續(xù)調(diào)用
getInstance() 直接返回就行了,每次都加鎖釋放鎖造成了不必要的開銷。
經(jīng)過一陣思索與回想之后,小雨記起了曾經(jīng)看過一個(gè)叫 Double-Checked Locking 的東東,雙重檢查鎖,嗯,再優(yōu)化一下,
public class Singleton { private static volatile Singleton instance; private
Singleton(){} public static final Singleton getInstance(){ if(instance == null)
{ synchronized (Singleton.class){ if(instance == null) { instance = new
Singleton(); } } } return instance; } }
單例在完成第一次實(shí)例化,后續(xù)再調(diào)用 getInstance()
先判空,如果不為空則直接返回,如果為空,就算兩個(gè)線程同時(shí)判斷為空,在同步塊中還做了一次雙重檢查,可以確保只會(huì)實(shí)例化一次,省去了不必要的加鎖開銷,同時(shí)也保證了線程安全。并且令小雨感到自我滿足的是他基于對(duì)JVM的一些了解加上了
volatile 關(guān)鍵字來避免實(shí)例化時(shí)由于指令重排序優(yōu)化可能導(dǎo)致的問題,真是畫龍點(diǎn)睛之筆啊。 簡直——完美!
Tips: volatile關(guān)鍵字的語義
*
保證變量對(duì)所有線程的可見性。對(duì)變量寫值的時(shí)候JMM(Java內(nèi)存模型)會(huì)將當(dāng)前線程的工作內(nèi)存值刷新到主內(nèi)存,讀的時(shí)候JMM會(huì)從主內(nèi)存讀取變量的值而不是從工作內(nèi)存讀取,確保一個(gè)變量值被一個(gè)線程更新后,另一個(gè)線程能立即讀取到更新后的值。
* 禁止指令重排序優(yōu)化。JVM在執(zhí)行程序時(shí)為了提高性能,編譯器和處理器常常會(huì)對(duì)指令做重排序,使用 volatile 可以禁止進(jìn)行指令重排序優(yōu)化。
JVM創(chuàng)建一個(gè)新的實(shí)例時(shí),主要需三步:
* 分配內(nèi)存
* 初始化構(gòu)造器
* 將對(duì)象引用指向分配的內(nèi)存地址
如果一個(gè)線程在實(shí)例化時(shí)JVM做了指令重排,比如先執(zhí)行了1,再執(zhí)行3,最后執(zhí)行2,則另一個(gè)線程可能獲取到一個(gè)還沒有完成初始化的對(duì)象引用,調(diào)用時(shí)可能導(dǎo)致問題,使用volatile可以禁止指令重排,避免這種問題。
小雨將答案交給面試官,面試官瞄了一眼說道:“基本可用了,但如果我用反射直接調(diào)用這個(gè)類的構(gòu)造函數(shù),是不是就不能保證單例了。”
小雨撓撓頭,對(duì)哦,如果使用反射就可以在運(yùn)行時(shí)改變單例構(gòu)造器的可見性,直接調(diào)用構(gòu)造器來創(chuàng)建一個(gè)新的實(shí)例了,比如通過下面這段代碼
Constructor<Singleton> constructor =
Singleton.class.getDeclaredConstructor(); constructor.setAccessible(true);
Singleton singleton = constructor.newInstance();
小雨再次陷入了思考。
反射安全的單例
怎么避免反射破壞單例呢,或許可以加一個(gè)靜態(tài)變量來控制,讓構(gòu)造器只有從 getInstance() 內(nèi)部調(diào)用才有效,不通過 getInstance()
直接調(diào)用則拋出異常,小雨按這個(gè)思路做了一番改造,
public class Singleton { private static volatile Singleton instance; private
static boolean flag = false; private Singleton(){ synchronized
(Singleton.class) { if (flag) { flag = false; } else { throw new
RuntimeException("Please use getInstance() method to get the single
instance."); } } } public static final Singleton getInstance(){ if(instance ==
null) { synchronized (Singleton.class){ if(instance == null) { flag = true;
instance = new Singleton(); } } } return instance; } }
使用靜態(tài)變量 flag 來控制,只有從 getInstance()
調(diào)用構(gòu)造器才能正常實(shí)例化,否則拋出異常。但馬上小雨就發(fā)現(xiàn)了存在的問題:既然可以通過反射來調(diào)用構(gòu)造器,那么也可以通過反射來改變 flag 的值,這樣苦心設(shè)置的
flag
控制邏輯不就被打破了嗎??磥硪矝]那么“完美”。雖然并不那么完美,但也一定程度上規(guī)避了使用反射直接調(diào)用構(gòu)造器的場景,并且貌似也想不出更好的辦法了,于是小雨提交了答案。
面試官露出迷之微笑:“想法挺好,反射的問題基本解決了,但如果我序列化這個(gè)單例對(duì)象,然后再反序列化出來一個(gè)對(duì)象,這兩個(gè)對(duì)象還一樣嗎,還能保證單例嗎。如果不能,怎么解決這個(gè)問題?”
SerializationSafeSingleton s1 = SerializationSafeSingleton.getInstance();
ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos
= new ObjectOutputStream(bos); oos.writeObject(s1); oos.close();
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis); SerializationSafeSingleton
s2 = (SerializationSafeSingleton) ois.readObject(); ois.close();
s1 == s2 嗎? 答案是否,如何解決呢。
序列化安全的單例
小雨思考了一會(huì),想起了曾經(jīng)學(xué)習(xí)序列化知識(shí)時(shí)接觸的 readResolve()
方法,該方法在ObjectInputStream已經(jīng)讀取一個(gè)對(duì)象并在準(zhǔn)備返回前調(diào)用,可以用來控制反序列化時(shí)直接返回一個(gè)對(duì)象,替換從流中讀取的對(duì)象,于是在前面實(shí)現(xiàn)的基礎(chǔ)上,小雨添加了一個(gè)
readResolve() 方法,
public class Singleton { private static volatile Singleton instance; private
static boolean flag = false; private Singleton(){ synchronized
(Singleton.class) { if (flag) { flag = false; } else { throw new
RuntimeException("Please use getInstance() method to get the single
instance."); } } } public static final Singleton getInstance(){ if(instance ==
null) { synchronized (Singleton.class){ if(instance == null) { flag = true;
instance = new Singleton(); } } } return instance; } /** * 該方法代替了從流中讀取對(duì)象 *
@return */ private Object readResolve(){ return getInstance(); } }
通過幾個(gè)步驟的逐步改造優(yōu)化,小雨完成了一個(gè)基本具備線程安全、反射安全、序列化安全的單例實(shí)現(xiàn),心想這下應(yīng)該足夠完美了吧。面試官臉上繼續(xù)保持著迷之微笑:“這個(gè)實(shí)現(xiàn)看起來還是顯得有點(diǎn)復(fù)雜,并且也不能完全解決反射安全的問題,想想看還有其它實(shí)現(xiàn)方案嗎。”
其它方案
小雨反復(fù)思考,前面的實(shí)現(xiàn)是通過加鎖來實(shí)現(xiàn)線程安全,除此之外,還可以通過類的加載機(jī)制來實(shí)現(xiàn)線程安全——類的靜態(tài)屬性只會(huì)在第一次加載類時(shí)初始化,并且在初始化的過程中,JVM是不允許其它線程來訪問的,于是又寫出了下面兩個(gè)版本
1.靜態(tài)初始化版本
public class Singleton { private static final Singleton instance = new
Singleton(); private Singleton(){} public static final Singleton getInstance()
{ return instance; } }
該版本借助JVM的類加載機(jī)制,本身線程安全,但只要 Singleton
類的某個(gè)靜態(tài)對(duì)象(方法或?qū)傩裕┍辉L問,就會(huì)造成實(shí)例的初始化,而該實(shí)例可能根本不會(huì)被用到,造成資源浪費(fèi),另一方面也存在反射與序列化的安全性問題,也需要進(jìn)行相應(yīng)的處理。
2.靜態(tài)內(nèi)部類版本
public class Singleton { private Singleton(){} public static final Singleton
getInstance() { return SingletonHolder.instance; } private static class
SingletonHolder { private static final Singleton instance = new Singleton(); } }
該版本只有在調(diào)用 getInstance()
才會(huì)進(jìn)行實(shí)例化,即延遲加載,避免資源浪費(fèi)的問題,同時(shí)也能保障線程安全,但是同樣存在反射與序列化的安全性問題,需要相應(yīng)處理。
這貌似跟前面版本的復(fù)雜性差不多啊,依然都需要解決反射與安全性的問題,小雨心想,有沒有一種既簡單又能避免這些問題的方案呢。
“完美”方案
一陣苦思冥想之后,小雨突然腦中靈光閃現(xiàn),枚舉?。ㄟ@也是《Effective Java》的作者推薦的方式啊)
public enum Singleton { INSTANCE; public void func(){ ... } }
可以直接通過 Singleton.INSTANCE
來引用單例,非常簡單的實(shí)現(xiàn),并且既是線程安全的,同時(shí)也能應(yīng)對(duì)反射與序列化的問題,面試官想要的估計(jì)就是它了吧。小雨再次提交了答案,這一次,面試官臉上的迷之微笑逐漸消失了……
Tips:為什么枚舉是線程、反射、序列化安全的?
*
枚舉實(shí)際是通過一個(gè)繼承自Enum的final類來實(shí)現(xiàn)(通過反編譯class文件可看到具體實(shí)現(xiàn)),在static代碼塊中對(duì)其成員進(jìn)行初始化,因此借助類加載機(jī)制來保障其線程安全
* 枚舉是不支持通過反射實(shí)例化的,在Constructor類的newInstance方法中可看到 if ((clazz.getModifiers() &
Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively
create enum objects");
*
枚舉在序列化的時(shí)候僅僅是將枚舉對(duì)象的name屬性輸出到結(jié)果中,反序列化的時(shí)候則是通過java.lang.Enum的valueOf方法來根據(jù)名字查找枚舉對(duì)象。并且,編譯器是不允許任何對(duì)這種序列化機(jī)制的定制的,禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。枚舉通過這種機(jī)制保障了序列化安全。
總結(jié)
枚舉方案近乎“完美”,但實(shí)際中,大部分情況下,我們使用雙重檢查鎖方案或靜態(tài)內(nèi)部類方案基本都能滿足我們的場景并能很好地運(yùn)行。并且方案從來沒有“完美”,只有更好或更合適。本文只是從單例實(shí)現(xiàn)的不斷演進(jìn)的過程中,了解或回顧如反射、序列化、線程安全、Java內(nèi)存模型(volatile語義)、JVM類加載機(jī)制、JVM指令重排序優(yōu)化等方面的知識(shí),同時(shí)也是啟示我們?cè)谠O(shè)計(jì)或?qū)崿F(xiàn)的過程中,多從各個(gè)角度思考,盡可能全面地考慮問題?;蛘撸谙嚓P(guān)面試中能更好地迎合面試官的“完美”期望。
作者:雨歌,一枚仍在學(xué)習(xí)路上的IT老兵
歡迎關(guān)注作者公眾號(hào):半路雨歌,一起學(xué)習(xí)成長
熱門工具 換一換