先說(shuō)一下我自己對(duì)單例模式的理解:
單例模式:在整個(gè)程序運(yùn)行周期內(nèi)本谜,某個(gè)類被設(shè)計(jì)為其所有實(shí)例都?xì)w屬于一個(gè)副本,以保證含義上的唯一性和行為上的總控性。這種類的設(shè)計(jì)方式被稱為單例模式。
如果某個(gè)類從現(xiàn)實(shí)世界角度來(lái)看丛肮,確實(shí)應(yīng)該只存在一個(gè)實(shí)例副本,或者該類的行為是作為整個(gè)系統(tǒng)中某個(gè)功能的總控統(tǒng)籌魄缚,將它通過(guò)單例模式來(lái)實(shí)現(xiàn),能夠提供良好的可維護(hù)性和準(zhǔn)確性焚廊,也更節(jié)省占用的內(nèi)存和新生成實(shí)例的開(kāi)銷冶匹。例如管理一個(gè)JDBC連接的類,或者一個(gè)Canvas中的畫筆類等咆瘟。
為了實(shí)現(xiàn)單例嚼隘,首先不能讓類被隨意地實(shí)例化。我們可以通過(guò)創(chuàng)建private構(gòu)造函數(shù)來(lái)屏蔽new關(guān)鍵字的調(diào)用袒餐。
在網(wǎng)上隨便搜一下飞蛹,能夠看到五花八門的單例模式說(shuō)明谤狡,各種名詞層出不窮,餓漢卧檐、懶漢墓懂、飽漢等等...但在這篇文章中我想層層遞進(jìn)地說(shuō)一下各種實(shí)現(xiàn)方式的進(jìn)化關(guān)系。
基本的單例
在我剛開(kāi)始寫代碼的時(shí)候霉囚,曾經(jīng)也遇到過(guò)需要只存在一個(gè)實(shí)例的場(chǎng)景捕仔。當(dāng)時(shí)還很懵懂,就寫出了如下的代碼:
// “懶漢” - 延遲初始化盈罐,非線程安全
// “懶”表現(xiàn)為: instance要等到真正使用時(shí)(getInstance)才會(huì)被實(shí)例化
public class Singleton1 {
private static Singleton1 instance;
private Singleton1 (){}
public static Singleton1 getInstance() {
if (instance == null) {
instance = new Singleton1();
}
return instance;
}
}
以上代碼是非線程安全的榜跌。
試想當(dāng)在多線程場(chǎng)景中,許多線程幾乎同時(shí)調(diào)用getInstance方法盅粪,并判斷instance == null為true,這個(gè)時(shí)候便會(huì)有多個(gè)現(xiàn)成去執(zhí)行實(shí)例化代碼钓葫。這樣一來(lái)便無(wú)法保證類的單例了。
而另外一種比較基本的單例寫法是:
//“餓漢”(其實(shí)我想稱它為“勤男”) - 立即初始化票顾,線程安全
//"餓"體現(xiàn)在: 餓漢很餓础浮,希望盡早吃到內(nèi)存
//"勤"體現(xiàn)在:人家一上來(lái)就把內(nèi)存區(qū)開(kāi)好了,相比于上面的“懶漢”库物,當(dāng)然是“勤”了
public class Singleton2 {
private static Singleton2 instance = new Singleton2();
private Singleton2 (){}
public static Singleton2 getInstance() {
return instance;
}
}
這種寫法使用Java的語(yǔ)法糖霸旗,能夠滿足多線程的并發(fā)需求。但是從延遲初始化的角度來(lái)說(shuō)戚揭,是有欠缺的诱告。尤其是當(dāng)Singleton是一個(gè)比較復(fù)雜的類,無(wú)法簡(jiǎn)單地通過(guò)new關(guān)鍵字進(jìn)行實(shí)例化民晒,或者需要獲得某些參數(shù)才能完成實(shí)例化時(shí)精居,延遲初始化就成了我們的必選項(xiàng)。
從上面我們可以看到潜必,兩種基本的單例實(shí)現(xiàn)方式靴姿,都存在各自的缺點(diǎn)。根據(jù)上面的分析磁滚,其實(shí)我們想要的是一種既能延遲初始化佛吓,又保證了多線程安全的單例實(shí)現(xiàn)模式。
延遲初始化+線程安全的單例
為了要保證二者兼得垂攘,我們?cè)诘谝环N代碼的基礎(chǔ)上维雇,增加能夠保證線程安全的代碼即可。為此晒他,我們引入synchronized關(guān)鍵字吱型。
synchronized關(guān)鍵字
synchronized關(guān)鍵字,可用來(lái)給對(duì)象和方法或者代碼塊加鎖陨仅,當(dāng)它鎖定一個(gè)方法或者一個(gè)代碼塊的時(shí)候津滞,同一時(shí)刻最多只有一個(gè)線程執(zhí)行這段代碼铝侵。另一個(gè)線程必須等待當(dāng)前線程執(zhí)行完這個(gè)代碼塊以后才能執(zhí)行該代碼塊。
由于是說(shuō)設(shè)計(jì)模式的文章触徐,這里就不展開(kāi)討論synchronized了咪鲜。按照上面的描述,如果沒(méi)研究過(guò)它的朋友應(yīng)該也能基本了解它的作用锌介。我們使用它來(lái)實(shí)現(xiàn)一個(gè)滿足兩個(gè)條件的單例:
//"懶漢"變種
//用 synchronized 修飾 getInstance方法
public class Singleton3 {
private static Singleton3 instance;
private Singleton3 (){}
public static synchronized Singleton3 getInstance() {
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}
這種寫法滿足了線程安全嗜诀,但是安全過(guò)頭了。在多線程場(chǎng)景中孔祸,同一時(shí)刻只能有一個(gè)線程訪問(wèn)getInstance方法隆敢,換言之就是多線程在這個(gè)方法上變成了單線程。大家可以想到崔慧,即使后續(xù)的線程中instance已經(jīng)不為null了拂蝎,但還是要等待前序線程執(zhí)行完該方法。這無(wú)疑是對(duì)效率的一大阻礙惶室。
Double Check Lock (DCL)
為了提升多線程效率温自,我們將synchronized換了個(gè)位置。但是為了確保單例皇钞,我們又在synchronized內(nèi)部增加了一次if判斷悼泌,這樣便有了兩次null檢查,即DCL:
//"懶漢"變種
//用 synchronized 修飾 getInstance方法
public class Singleton4 {
private static Singleton4 instance;
private Singleton4 (){}
public static synchronized Singleton4 getInstance() {
if (instance == null) {
synchronized(Singleton4.class){
if(instance == null) {
instance = new Singleton4();
}
}
}
return instance;
}
}
由于synchronized作用域已經(jīng)從一個(gè)方法縮小到了一段代碼塊夹界,多個(gè)線程可以同時(shí)訪問(wèn)第一個(gè)if判斷馆里,如果instance不為null便可以直接返回,不用等待可柿。這種寫法雖然奇怪鸠踪,但是看起來(lái)確實(shí)實(shí)現(xiàn)了延時(shí)初始化和線程安全,并且提升了多線程的效率复斥。
但是實(shí)際上营密,這種方式并沒(méi)有保證完全的線程安全,罪魁禍?zhǔn)妆闶?strong>指令重排序目锭。
指令重排序
instance = new Singleton4();
這行代碼评汰,可以被編譯器編成以下三行指令:
- rawMemory = allocateMemory(); //分配內(nèi)存
- preparedMemory = initMemory(rawMemory); //初始化內(nèi)存
- instance = preparedMemory; //內(nèi)存與字段綁定
試問(wèn)如果編譯器將結(jié)果優(yōu)化為以下序列,將后兩個(gè)指令調(diào)換順序痢虹,多線程情況下會(huì)出現(xiàn)什么結(jié)果键俱?
- 分配內(nèi)存
- 內(nèi)存與字段綁定
- 初始化內(nèi)存
有的線程可能在剛進(jìn)入該方法時(shí),剛好上述指令執(zhí)行到了第二步世分,因此instance不為null。但實(shí)際上此時(shí)instance還沒(méi)有被初始化完成缀辩,線程拿到的是一個(gè)殘缺的非null實(shí)例臭埋。
volatile關(guān)鍵字
volatile關(guān)鍵字保證了對(duì)應(yīng)的字段能夠含有一下特性:
- 保證了不同線程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見(jiàn)性踪央,即一個(gè)線程修改變量值,這新值對(duì)其他線程來(lái)說(shuō)是立即可見(jiàn)的瓢阴。
- 禁止對(duì)操作該變量的指令重排序畅蹂。
具體關(guān)于volatile關(guān)鍵字的說(shuō)明可以參見(jiàn)網(wǎng)上其他文章,這里也不再贅述荣恐,大家只要先了解上述兩個(gè)特性液斜。下面我們來(lái)利用volatile對(duì)我們的單例模式進(jìn)行進(jìn)一步的加工:
//"懶漢"變種
//用 volatile 修飾 instance
public class Singleton5 {
private static volatile Singleton5 instance = null;
private Singleton5 (){}
public static synchronized Singleton5 getInstance() {
if (instance == null) {
synchronized(Singleton5.class){
if(instance == null) {
instance = new Singleton4();
}
}
}
return instance;
}
}
通過(guò)使用volatile修飾instance,保證了實(shí)例化時(shí)指令的正確順序叠穆,也確保了多線程安全少漆。這種寫法基本上實(shí)現(xiàn)了一個(gè)單例的基本要求。
套路的單例
Holder模式
除了上面組合使用synchronized和volatile進(jìn)行多線程安全保護(hù)外硼被,我們還可以按照Holder方式將基本的實(shí)例中第二種“勤漢”模式進(jìn)行修改示损,從而再實(shí)現(xiàn)一套即延遲初始化,又保證線程安全的代碼:
//Holder模式
//引入靜態(tài)類嚷硫,該類在首次實(shí)際使用時(shí)進(jìn)行內(nèi)存分配检访,即return SingletonHolder.INSTANCE時(shí)
public class Singleton6 {
private static class SingletonHolder {
private static final Singleton6 INSTANCE = new Singleton6();
}
private Singleton6 (){}
public static final Singleton6 getInstance() {
return SingletonHolder.INSTANCE;
}
}
枚舉模式(Since jdk1.5)
由于枚舉的特性,它實(shí)際上是一個(gè)天然的單例仔掸,確保了實(shí)例副本的唯一性:
public enum Singleton7{
INSTANCE;
}
枚舉型的單例能夠支持線程安全脆贵,且能夠?qū)崿F(xiàn)延遲加載,并且還能防止反序列化問(wèn)題和反射攻擊問(wèn)題起暮,不愧為Effective Java中提出的完美的單例解決方案卖氨。
反序列化問(wèn)題和反射問(wèn)題
這一塊內(nèi)容完全是為了對(duì)單例模式進(jìn)行補(bǔ)充,如果只是為了了解設(shè)計(jì)模式的話鞋怀,可以不再往下閱讀了双泪。
由于單例模式最重要的一點(diǎn)就是保證該類的實(shí)例副本的唯一性,而如果這個(gè)類支持序列化密似,那么在反序列化的時(shí)候我便可以產(chǎn)生多個(gè)實(shí)例焙矛,即反序列化是一個(gè)隱藏很深的構(gòu)造函數(shù)。如果不對(duì)這種情況進(jìn)行封鎖残腌,勢(shì)必會(huì)破壞單例村斟。
private Singleton readResolve(){
return getInstance();
}
通過(guò)實(shí)現(xiàn)readResolve方法,在反序列化時(shí)抛猫,跳過(guò)默認(rèn)邏輯蟆盹,而使用已經(jīng)寫好的getInstance方法,能夠規(guī)避這種問(wèn)題闺金。
而反射問(wèn)題則是利用Java的反射機(jī)制逾滥,調(diào)用到private訪問(wèn)權(quán)限的構(gòu)造函數(shù),從而生成了多個(gè)實(shí)例败匹。針對(duì)這個(gè)問(wèn)題寨昙,目前可以看到的是枚舉模式能夠完美地進(jìn)行處理讥巡。