前言
HI,歡迎來(lái)到《每周一博》蛾派。今天是十月第五周俄认,我給大家簡(jiǎn)單介紹一下單例模式。
單例模式是最簡(jiǎn)單的模式洪乍,也是應(yīng)用最廣的模式之一眯杏。有時(shí)整個(gè)系統(tǒng)只需要一個(gè)全局對(duì)象,這樣有利于協(xié)調(diào)系統(tǒng)整體的行為壳澳,比如硬件資源岂贩,數(shù)據(jù)庫(kù)等,這種不能由用戶自由創(chuàng)建對(duì)象的場(chǎng)景巷波,就適合使用單例模式萎津。
單例設(shè)計(jì)模式的優(yōu)點(diǎn):
- 在內(nèi)存中只有一個(gè)實(shí)例,減少了內(nèi)存開支抹镊,尤其是當(dāng)一個(gè)對(duì)象需要頻繁創(chuàng)建銷毀锉屈,并且創(chuàng)建或銷毀時(shí)性能無(wú)法優(yōu)化,單例模式的優(yōu)勢(shì)就非常明顯垮耳;
- 單例模式只生成了一個(gè)實(shí)例颈渊,減少了系統(tǒng)性能開銷,當(dāng)一個(gè)對(duì)象的產(chǎn)生需要較多資源時(shí),如讀取配置儡炼,產(chǎn)生其他依賴對(duì)象時(shí)妓湘,可以通過(guò)應(yīng)用啟動(dòng)時(shí)直接產(chǎn)生一個(gè)單例對(duì)象查蓉,然后永久駐留內(nèi)存的方式來(lái)解決驶沼;
- 單例模式可以避免對(duì)資源的多重占用魄梯,比如避免對(duì)同一個(gè)文件同時(shí)進(jìn)行寫操作;
- 單例模式可以在系統(tǒng)設(shè)置全局的訪問(wèn)點(diǎn),優(yōu)化和共享資源訪問(wèn)呕寝,比如設(shè)計(jì)一個(gè)單例類,負(fù)責(zé)所有數(shù)據(jù)表的映射處理橙垢,安卓源碼的系統(tǒng)服務(wù)就是這么用的豺憔;
單例設(shè)計(jì)模式的缺點(diǎn):
- 沒(méi)有抽象層接口,擴(kuò)展困難霜浴,只能修改代碼晶衷,不適用于變化對(duì)象;
- 如果持有Context引用要使用Application Context阴孟,否則會(huì)造成內(nèi)存泄漏晌纫;
實(shí)現(xiàn)單例模式需要注意的地方:
A. 保證構(gòu)造函數(shù)是私有的;
B. 對(duì)外提供一個(gè)靜態(tài)方法來(lái)返回對(duì)象永丝;
C. 要保證線程安全锹漱;
D. 反序列化也要保證對(duì)象唯一;
單例模式的實(shí)現(xiàn)主要有3種:
- 餓漢式:
直接創(chuàng)建對(duì)象
// 直接創(chuàng)建靜態(tài)變量
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){}
public static synchronized Singleton getInstance(){
return instance;
}
}
// 或者通過(guò)靜態(tài)初始化塊
public class Singleton {
private static Singleton instance ;
static {
instance = new Singleton();
}
private Singleton(){}
public static synchronized Singleton getInstance(){
return instance;
}
}
優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單慕嚷,線程安全哥牍;
缺點(diǎn):不管該類有沒(méi)有用到都會(huì)創(chuàng)建對(duì)象,消耗資源
- Double CheckLock:
線程安全的懶漢式喝检,對(duì)象需要時(shí)才進(jìn)行創(chuàng)建
public class Singleton {
private static volatile Singleton instance;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
if(instance ==null){
instance = new Singleton();
}
}
}
return instance;
}
}
該實(shí)現(xiàn)方式會(huì)增加同步鎖來(lái)保證線程安全嗅辣,注意先判空,再同步鎖挠说,再判空澡谭,這樣就能夠做到效率和安全的雙重保證。那么為什么要進(jìn)行2次判空呢纺涤?
new一個(gè)對(duì)象并不是一個(gè)原子操作译暂,它會(huì)編譯成多條匯編指令,主要做了3件事:
- 給對(duì)象實(shí)例分配內(nèi)存撩炊;
- 調(diào)用構(gòu)造函數(shù)外永,初始化成員字段;
- 將對(duì)象紙箱分配的內(nèi)存空間拧咳,此時(shí)對(duì)象就不是null了伯顶;
由于Java編譯器允許處理器亂序執(zhí)行,所以第2步和第3步的先后順序是不確定的,當(dāng)兩個(gè)線程同時(shí)到達(dá)后就可能會(huì)創(chuàng)建2個(gè)實(shí)例祭衩。
Java語(yǔ)言提供了一種稍弱的同步機(jī)制灶体,即volatile變量,用來(lái)確保將變量的更新操作通知到其他線程掐暮。當(dāng)把變量聲明為volatile類型后蝎抽,編譯器與運(yùn)行時(shí)都會(huì)注意到這個(gè)變量是共享的,因此不會(huì)將該變量上的操作與其他內(nèi)存操作一起重排序路克。volatile變量不會(huì)被緩存在寄存器或者對(duì)其他處理器不可見的地方樟结,因此在讀取volatile類型的變量時(shí)總會(huì)返回最新寫入的值。
在訪問(wèn)volatile變量時(shí)不會(huì)執(zhí)行加鎖操作精算,因此也就不會(huì)使執(zhí)行線程阻塞瓢宦,因此volatile變量是一種比sychronized關(guān)鍵字更輕量級(jí)的同步機(jī)制。
- 靜態(tài)內(nèi)部類:
利用靜態(tài)類只會(huì)加載一次的機(jī)制灰羽,使用靜態(tài)內(nèi)部類持有單例對(duì)象驮履,達(dá)到單例的效果
public class Singleton {
private Singleton(){}
public static synchronized Singleton getInstance(){
return SingletonHolder.instance;
}
private static class SingletonHolder{
private static Singleton instance = new Singleton();
}
}
靜態(tài)內(nèi)部類的優(yōu)點(diǎn):
外部類加載時(shí)并不需要立即加載內(nèi)部類,內(nèi)部類不被加載則不去初始化instance廉嚼,故而不占內(nèi)存玫镐。即當(dāng)SingleTon第一次被加載時(shí),并不需要去加載SingletonHolder前鹅,只有當(dāng)getInstance()方法第一次被調(diào)用時(shí)摘悴,才會(huì)去初始化instance,第一次調(diào)用getInstance()方法會(huì)導(dǎo)致虛擬機(jī)加載SingletonHolder類,這種方法不僅能確保線程安全舰绘,也能保證單例的唯一性蹂喻,同時(shí)也延遲了單例的實(shí)例化。由于不用同步鎖機(jī)制捂寿,性能也會(huì)有所提升口四。
那靜態(tài)內(nèi)部類又是如何實(shí)現(xiàn)線程安全的呢?我們先了解下類的加載時(shí)機(jī)秦陋。JAVA虛擬機(jī)在有且僅有的5種場(chǎng)景下會(huì)對(duì)類進(jìn)行初始化:
- new一個(gè)關(guān)鍵字或者一個(gè)實(shí)例化對(duì)象蔓彩;
讀取或設(shè)置一個(gè)靜態(tài)字段時(shí)(final修飾,已在編譯期把結(jié)果放入常量池的除外)驳概;
調(diào)用一個(gè)類的靜態(tài)方法時(shí)赤嚼; - 對(duì)類進(jìn)行反射調(diào)用的時(shí)候,如果類沒(méi)進(jìn)行初始化顺又,需要先調(diào)用其初始化方法進(jìn)行初始化更卒;
- 初始化一個(gè)類時(shí),如果其父類還未進(jìn)行初始化稚照,會(huì)先觸發(fā)其父類的初始化蹂空;
- 當(dāng)虛擬機(jī)啟動(dòng)時(shí)俯萌,用戶需要指定一個(gè)要執(zhí)行的主類(包含main()方法的類),虛擬機(jī)會(huì)先初始化這個(gè)類上枕;
- 當(dāng)使用動(dòng)態(tài)語(yǔ)言支持時(shí)咐熙,如果一個(gè)MethodHandle實(shí)例最后的解析結(jié)果REF_getStatic、REF_putStatic辨萍、REF_invokeStatic的方法句柄棋恼,并且這個(gè)方法句柄所對(duì)應(yīng)的類沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化分瘦;
這5種情況被稱為是類的主動(dòng)引用蘸泻,除此之外的所有引用類都不會(huì)對(duì)類進(jìn)行初始化琉苇,稱為被動(dòng)引用嘲玫,靜態(tài)內(nèi)部類就屬于被動(dòng)引用的行列。
我們?cè)倏磄etInstance()方法并扇,取的是SingletonHolder里的instance對(duì)象去团,跟上面那個(gè)DCL方法不同的是,getInstance()方法并沒(méi)有多次去new對(duì)象穷蛹,故不管多少個(gè)線程去調(diào)用getInstance()方法土陪,取的都是同一個(gè)instance對(duì)象,而不用去重新創(chuàng)建肴熏。當(dāng)getInstance()方法被調(diào)用時(shí)鬼雀,SingletonHolder才在SingletonHolder的運(yùn)行時(shí)常量池里,把符號(hào)引用替換為直接引用蛙吏,這時(shí)靜態(tài)對(duì)象instance也真正被創(chuàng)建源哩,然后再被getInstance()方法返回出去。那么instance在創(chuàng)建過(guò)程中又是如何保證線程安全的呢鸦做?
虛擬機(jī)會(huì)保證一個(gè)類的clinit方法在多線程環(huán)境中被正確地加鎖励烦、同步,如果多個(gè)線程同時(shí)去初始化一個(gè)類泼诱,那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類的clinit方法坛掠,其他線程都需要阻塞等待,直到活動(dòng)線程執(zhí)行clinit方法完畢治筒。如果在一個(gè)類的clinit方法中有耗時(shí)很長(zhǎng)的操作屉栓,就可能造成多個(gè)進(jìn)程阻塞(需要注意的是,其他線程雖然會(huì)被阻塞耸袜,但如果執(zhí)行clinit方法后友多,其他線程喚醒之后不會(huì)再次進(jìn)入clinit方法。同一個(gè)加載器下句灌,一個(gè)類型只會(huì)初始化一次)夷陋,在實(shí)際應(yīng)用中欠拾,這種阻塞往往是很隱蔽的。
故而骗绕,可以看出INSTANCE在創(chuàng)建過(guò)程中是線程安全的藐窄,所以說(shuō)靜態(tài)內(nèi)部類形式的單例可保證線程安全,也能保證單例的唯一性酬土,同時(shí)也延遲了單例的實(shí)例化荆忍。
靜態(tài)內(nèi)部類實(shí)現(xiàn)的缺點(diǎn):
傳遞參數(shù)問(wèn)題,由于是靜態(tài)內(nèi)部類的形式去創(chuàng)建單例的撤缴,故外部無(wú)法傳遞參數(shù)進(jìn)去刹枉,所以,我們創(chuàng)建單例時(shí)屈呕,可以在靜態(tài)內(nèi)部類與DCL模式下權(quán)衡微宝。
最后總結(jié):
以上三種實(shí)現(xiàn)方式都是可以達(dá)到線程安全的效果,我們推薦使用靜態(tài)內(nèi)部類的方式虎眨,另外還有枚舉也可以實(shí)現(xiàn)單例蟋软,它不僅能避免多線程同步問(wèn)題,而且還能防止反射或反序列化重新創(chuàng)建新的對(duì)象嗽桩,但是安卓官方不推薦岳守,因?yàn)閮?nèi)存消耗是靜態(tài)常量的兩倍。
public enum EnumMode {
INSTANCE;
}
上述三種實(shí)現(xiàn)方式都需要考慮到反序列化的問(wèn)題碌冶,如果Singleton實(shí)現(xiàn)了Serializable接口湿痢,那么這個(gè)類的實(shí)例就可能被序列化和復(fù)原。不管怎樣扑庞,如果序列化一個(gè)單例類的對(duì)象譬重,那么復(fù)原多個(gè)之后就會(huì)有多個(gè)單例類的實(shí)例,因?yàn)樾枰貙憆eadResolve()方法嫩挤,在該方法里返回instance害幅,防止反序列化得到多個(gè)對(duì)象。
private Object readResolve(){
return instance;
}
結(jié)尾:
本周給大家簡(jiǎn)單介紹了單例模式的常見三種實(shí)現(xiàn)方式岂昭,其中雙重鎖和靜態(tài)內(nèi)部類兩種方式涉及到了Java虛擬機(jī)創(chuàng)建對(duì)象的過(guò)程以现,這些內(nèi)容還需要深入了解下。感謝大家的閱讀约啊,我們下周再見邑遏。