轉(zhuǎn)載:https://blog.csdn.net/mnb65482/article/details/80458571
本文主要介紹java的單例模式我磁,以及詳細(xì)剖析靜態(tài)內(nèi)部類(lèi)之所以能夠?qū)崿F(xiàn)單例的原理。OK,廢話不多說(shuō)汽纠,進(jìn)入正文媳危。
首先我們要先了解下單例的四大原則:
1.構(gòu)造私有歪玲。
2.以靜態(tài)方法或者枚舉返回實(shí)例呐舔。
3.確保實(shí)例只有一個(gè)淑趾,尤其是多線程環(huán)境烹棉。
4.確保反序列換時(shí)不會(huì)重新構(gòu)建對(duì)象攒霹。
我們常用的單例模式有:
餓漢模式、懶漢模式浆洗、雙重鎖懶漢模式催束、靜態(tài)內(nèi)部類(lèi)模式、枚舉模式伏社,我們來(lái)逐一分析下這些模式的區(qū)別抠刺。
1.餓漢模式:
public class SingleTon{
private static SingleTon INSTANCE = new SingleTon();
private SingleTon(){}
public static SingleTon getInstance(){ return INSTANCE; }}
餓漢模式在類(lèi)被初始化時(shí)就已經(jīng)在內(nèi)存中創(chuàng)建了對(duì)象,以空間換時(shí)間洛口,故不存在線程安全問(wèn)題矫付。
2.懶漢模式:
public class SingleTon{
private static SingleTon INSTANCE = null;
private SingleTon(){}
public static SingleTon getInstance() {
if(INSTANCE == null){
INSTANCE = new SingleTon();
}
return INSTANCE;
}
}
懶漢模式在方法被調(diào)用后才創(chuàng)建對(duì)象第焰,以時(shí)間換空間买优,在多線程環(huán)境下存在風(fēng)險(xiǎn)。
3.雙重鎖懶漢模式(Double Check Lock)
public class SingleTon{
private static SingleTon INSTANCE = null;
private SingleTon(){}
public static SingleTon getInstance(){if(INSTANCE == null){
synchronized(SingleTon.class){
if(INSTANCE == null){
INSTANCE = new SingleTon();
}
}
return INSTANCE;
}
}
}
DCL模式的優(yōu)點(diǎn)就是挺举,只有在對(duì)象需要被使用時(shí)才創(chuàng)建杀赢,第一次判斷 INSTANCE == null為了避免非必要加鎖,當(dāng)?shù)谝淮渭虞d時(shí)才對(duì)實(shí)例進(jìn)行加鎖再實(shí)例化湘纵。這樣既可以節(jié)約內(nèi)存空間脂崔,又可以保證線程安全。但是梧喷,由于jvm存在亂序執(zhí)行功能砌左,DCL也會(huì)出現(xiàn)線程不安全的情況脖咐。具體分析如下:
INSTANCE = new SingleTon();
這個(gè)步驟,其實(shí)在jvm里面的執(zhí)行分為三步:
1.在堆內(nèi)存開(kāi)辟內(nèi)存空間汇歹。
2.在堆內(nèi)存中實(shí)例化SingleTon里面的各個(gè)參數(shù)屁擅。
3.把對(duì)象指向堆內(nèi)存空間。
由于jvm存在亂序執(zhí)行功能产弹,所以可能在2還沒(méi)執(zhí)行時(shí)就先執(zhí)行了3派歌,如果此時(shí)再被切換到線程B上,由于執(zhí)行了3痰哨,INSTANCE 已經(jīng)非空了胶果,會(huì)被直接拿出來(lái)用,這樣的話斤斧,就會(huì)出現(xiàn)異常早抠。這個(gè)就是著名的DCL失效問(wèn)題。
不過(guò)在JDK1.5之后折欠,官方也發(fā)現(xiàn)了這個(gè)問(wèn)題贝或,故而具體化了volatile吼过,即在JDK1.6及以后锐秦,只要定義為private volatile static SingleTon INSTANCE = null;就可解決DCL失效問(wèn)題。volatile確保INSTANCE每次均在主內(nèi)存中讀取盗忱,這樣雖然會(huì)犧牲一點(diǎn)效率酱床,但也無(wú)傷大雅。
3.靜態(tài)內(nèi)部類(lèi)模式:
public class SingleTon{
private SingleTon(){}
private static class SingleTonHoler{
private static SingleTon INSTANCE = new SingleTon();
}
public static SingleTon getInstance(){
return SingleTonHoler.INSTANCE;
}
}
靜態(tài)內(nèi)部類(lèi)的優(yōu)點(diǎn)是:外部類(lèi)加載時(shí)并不需要立即加載內(nèi)部類(lèi)趟佃,內(nèi)部類(lèi)不被加載則不去初始化INSTANCE扇谣,故而不占內(nèi)存。即當(dāng)SingleTon第一次被加載時(shí)闲昭,并不需要去加載SingleTonHoler罐寨,只有當(dāng)getInstance()方法第一次被調(diào)用時(shí),才會(huì)去初始化INSTANCE,第一次調(diào)用getInstance()方法會(huì)導(dǎo)致虛擬機(jī)加載SingleTonHoler類(lèi)序矩,這種方法不僅能確保線程安全鸯绿,也能保證單例的唯一性,同時(shí)也延遲了單例的實(shí)例化簸淀。
那么瓶蝴,靜態(tài)內(nèi)部類(lèi)又是如何實(shí)現(xiàn)線程安全的呢?首先租幕,我們先了解下類(lèi)的加載時(shí)機(jī)舷手。
類(lèi)加載時(shí)機(jī):JAVA虛擬機(jī)在有且僅有的5種場(chǎng)景下會(huì)對(duì)類(lèi)進(jìn)行初始化。
1.遇到new劲绪、getstatic男窟、setstatic或者invokestatic這4個(gè)字節(jié)碼指令時(shí)盆赤,對(duì)應(yīng)的java代碼場(chǎng)景為:new一個(gè)關(guān)鍵字或者一個(gè)實(shí)例化對(duì)象時(shí)、讀取或設(shè)置一個(gè)靜態(tài)字段時(shí)(final修飾歉眷、已在編譯期把結(jié)果放入常量池的除外)弟劲、調(diào)用一個(gè)類(lèi)的靜態(tài)方法時(shí)。
2.使用java.lang.reflect包的方法對(duì)類(lèi)進(jìn)行反射調(diào)用的時(shí)候姥芥,如果類(lèi)沒(méi)進(jìn)行初始化兔乞,需要先調(diào)用其初始化方法進(jìn)行初始化。
3.當(dāng)初始化一個(gè)類(lèi)時(shí)凉唐,如果其父類(lèi)還未進(jìn)行初始化庸追,會(huì)先觸發(fā)其父類(lèi)的初始化。
4.當(dāng)虛擬機(jī)啟動(dòng)時(shí)台囱,用戶(hù)需要指定一個(gè)要執(zhí)行的主類(lèi)(包含main()方法的類(lèi))淡溯,虛擬機(jī)會(huì)先初始化這個(gè)類(lèi)。
5.當(dāng)使用JDK 1.7等動(dòng)態(tài)語(yǔ)言支持時(shí)簿训,如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果REF_getStatic咱娶、REF_putStatic、REF_invokeStatic的方法句柄强品,并且這個(gè)方法句柄所對(duì)應(yīng)的類(lèi)沒(méi)有進(jìn)行過(guò)初始化膘侮,則需要先觸發(fā)其初始化。
這5種情況被稱(chēng)為是類(lèi)的主動(dòng)引用的榛,注意琼了,這里《虛擬機(jī)規(guī)范》中使用的限定詞是"有且僅有",那么夫晌,除此之外的所有引用類(lèi)都不會(huì)對(duì)類(lèi)進(jìn)行初始化雕薪,稱(chēng)為被動(dòng)引用。靜態(tài)內(nèi)部類(lèi)就屬于被動(dòng)引用的行列晓淀。
我們?cè)倩仡^看下getInstance()方法所袁,調(diào)用的是SingleTonHoler.INSTANCE,取的是SingleTonHoler里的INSTANCE對(duì)象凶掰,跟上面那個(gè)DCL方法不同的是燥爷,getInstance()方法并沒(méi)有多次去new對(duì)象,故不管多少個(gè)線程去調(diào)用getInstance()方法锄俄,取的都是同一個(gè)INSTANCE對(duì)象局劲,而不用去重新創(chuàng)建。當(dāng)getInstance()方法被調(diào)用時(shí)奶赠,SingleTonHoler才在SingleTon的運(yùn)行時(shí)常量池里鱼填,把符號(hào)引用替換為直接引用,這時(shí)靜態(tài)對(duì)象INSTANCE也真正被創(chuàng)建毅戈,然后再被getInstance()方法返回出去苹丸,這點(diǎn)同餓漢模式愤惰。那么INSTANCE在創(chuàng)建過(guò)程中又是如何保證線程安全的呢?在《深入理解JAVA虛擬機(jī)》中赘理,有這么一句話:
虛擬機(jī)會(huì)保證一個(gè)類(lèi)的<clinit>()方法在多線程環(huán)境中被正確地加鎖宦言、同步,如果多個(gè)線程同時(shí)去初始化一個(gè)類(lèi)商模,那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類(lèi)的<clinit>()方法奠旺,其他線程都需要阻塞等待,直到活動(dòng)線程執(zhí)行<clinit>()方法完畢施流。如果在一個(gè)類(lèi)的<clinit>()方法中有耗時(shí)很長(zhǎng)的操作响疚,就可能造成多個(gè)進(jìn)程阻塞(需要注意的是,其他線程雖然會(huì)被阻塞瞪醋,但如果執(zhí)行<clinit>()方法后忿晕,其他線程喚醒之后不會(huì)再次進(jìn)入<clinit>()方法。同一個(gè)加載器下银受,一個(gè)類(lèi)型只會(huì)初始化一次践盼。),在實(shí)際應(yīng)用中宾巍,這種阻塞往往是很隱蔽的咕幻。
故而,可以看出INSTANCE在創(chuàng)建過(guò)程中是線程安全的蜀漆,所以說(shuō)靜態(tài)內(nèi)部類(lèi)形式的單例可保證線程安全谅河,也能保證單例的唯一性咱旱,同時(shí)也延遲了單例的實(shí)例化确丢。
那么,是不是可以說(shuō)靜態(tài)內(nèi)部類(lèi)單例就是最完美的單例模式了呢吐限?其實(shí)不然鲜侥,靜態(tài)內(nèi)部類(lèi)也有著一個(gè)致命的缺點(diǎn),就是傳參的問(wèn)題诸典,由于是靜態(tài)內(nèi)部類(lèi)的形式去創(chuàng)建單例的描函,故外部無(wú)法傳遞參數(shù)進(jìn)去,例如Context這種參數(shù)狐粱,所以舀寓,我們創(chuàng)建單例時(shí),可以在靜態(tài)內(nèi)部類(lèi)與DCL模式里自己斟酌肌蜻。
最后粗略的介紹下枚舉類(lèi)型的單例吧互墓。
枚舉單例:
public enum SingleTon{
INSTANCE;
public void method(){
//TODO
}
}
枚舉在java中與普通類(lèi)一樣,都能擁有字段與方法蒋搜,而且枚舉實(shí)例創(chuàng)建是線程安全的篡撵,在任何情況下判莉,它都是一個(gè)單例。我們可直接以
SingleTon.INSTANCE
的方式調(diào)用育谬。