原創(chuàng)文章,轉(zhuǎn)載請標(biāo)注出處:《Java設(shè)計模式系列-單例模式》
一纷纫、概述
????????所謂單例枕扫,指的就是單實例,有且僅有一個類實例辱魁,這個單例不應(yīng)該由人來控制烟瞧,而應(yīng)該由代碼來限制,強(qiáng)制單例染簇。
????????單例有其獨有的使用場景参滴,一般是對于那些業(yè)務(wù)邏輯上限定不能多例只能單例的情況,例如:類似于計數(shù)器之類的存在剖笙,一般都需要使用一個實例來進(jìn)行記錄卵洗,若多例計數(shù)則會不準(zhǔn)確。
????????其實單例就是那些很明顯的使用場合弥咪,沒有之前學(xué)習(xí)的那些模式所使用的復(fù)雜場景过蹂,只要你需要使用單例,那你就使用單例聚至,簡單易理解酷勺。
????????所以我認(rèn)為有關(guān)單例模式的重點不在于場景,而在于如何使用扳躬。
二脆诉、單例實現(xiàn)
2.1 懶漢式
????????何為懶?顧名思義贷币,就是不做事击胜,這里也是同義,懶漢式就是不在系統(tǒng)加載時就創(chuàng)建類的單例役纹,而是在第一次使用實例的時候再創(chuàng)建偶摔。
????????詳見下方代碼示例:
public class LHanDanli {
// 定義一個私有類變量來存放單例,私有的目的是指外部無法
// 直接獲取這個變量促脉,而要使用提供的公共方法來獲取
private static LHanDanli dl = null;
// 定義私有構(gòu)造器辰斋,表示只在類內(nèi)部使用,
// 亦指單例的實例只能在單例類內(nèi)部創(chuàng)建
private LHanDanli(){}
// 定義一個公共的公開的方法來返回該類的實例瘸味,
// 由于是懶漢式宫仗,需要在第一次使用時生成實例,
// 所以為了線程安全旁仿,使用synchronized關(guān)鍵字來確保只會生成單例
public static synchronized LHanDanli getInstance(){
if(dl == null){
dl = new LHanDanli();
}
return dl;
}
}
2.2 餓漢式
????????又何為餓藕夫?餓者,饑不擇食;但凡有食汁胆,必急食之梭姓。此處同義:在加載類的時候就會創(chuàng)建類的單例,并保存在類中嫩码。
????????詳見下方代碼示例:
public class EHanDanli {
// 此處定義類變量實例并直接實例化,
// 在類加載的時候就完成了實例化并保存在類中
private static EHanDanli dl = new EHanDanli();
// 定義無參構(gòu)造器罪既,用于單例實例
private EHanDanli(){}
// 定義公開方法铸题,返回已創(chuàng)建的單例
public static EHanDanli getInstance(){
return dl;
}
}
2.3 雙重加鎖機(jī)制
????????何為雙重加鎖機(jī)制?
????????在懶漢式實現(xiàn)單例模式的代碼中琢感,有使用synchronized關(guān)鍵字來同步獲取實例丢间,保證單例的唯一性,但是上面的代碼在每一次執(zhí)行時都要進(jìn)行同步和判斷驹针,無疑會拖慢速度烘挫,使用雙重加鎖機(jī)制正好可以解決這個問題:
public class SLHanDanli {
private static volatile SLHanDanli dl = null;
private SLHanDanli(){}
public static SLHanDanli getInstance(){
if(dl == null){
synchronized (SLHanDanli.class) {
if(dl == null){
dl = new SLHanDanli();
}
}
}
return dl;
}
}
????????看了上面的代碼,有沒有感覺很無語柬甥,雙重加鎖難道不是需要兩個synchronized進(jìn)行加鎖的嗎饮六?
????????...
????????其實不然,這里的雙重指的的雙重判斷苛蒲,而加鎖單指那個synchronized卤橄,為什么要進(jìn)行雙重判斷,其實很簡單臂外,第一重判斷窟扑,如果單例已經(jīng)存在,那么就不再需要進(jìn)行同步操作漏健,而是直接返回這個實例嚎货,如果沒有創(chuàng)建,才會進(jìn)入同步塊蔫浆,同步塊的目的與之前相同殖属,目的是為了防止有兩個調(diào)用同時進(jìn)行時,導(dǎo)致生成多個實例克懊,有了同步塊忱辅,每次只能有一個線程調(diào)用能訪問同步塊內(nèi)容,當(dāng)?shù)谝粋€搶到鎖的調(diào)用獲取了實例之后谭溉,這個實例就會被創(chuàng)建墙懂,之后的所有調(diào)用都不會進(jìn)入同步塊,直接在第一重判斷就返回了單例扮念。至于第二個判斷损搬,個人感覺有點查遺補(bǔ)漏的意味在內(nèi)(期待高人高見)。
????????補(bǔ)充:關(guān)于鎖內(nèi)部的第二重空判斷的作用,當(dāng)多個線程一起到達(dá)鎖位置時巧勤,進(jìn)行鎖競爭嵌灰,其中一個線程獲取鎖,如果是第一次進(jìn)入則dl為null颅悉,會進(jìn)行單例對象的創(chuàng)建沽瞭,完成后釋放鎖,其他線程獲取鎖后就會被空判斷攔截剩瓶,直接返回已創(chuàng)建的單例對象驹溃。
????????不論如何,使用了雙重加鎖機(jī)制后延曙,程序的執(zhí)行速度有了顯著提升豌鹤,不必每次都同步加鎖。
????????其實我最在意的是volatile的使用枝缔,volatile關(guān)鍵字的含義是:被其所修飾的變量的值不會被本地線程緩存布疙,所有對該變量的讀寫都是直接操作共享內(nèi)存來實現(xiàn),從而確保多個線程能正確的處理該變量愿卸。該關(guān)鍵字可能會屏蔽掉虛擬機(jī)中的一些代碼優(yōu)化灵临,所以其運行效率可能不是很高,所以擦酌,一般情況下俱诸,并不建議使用雙重加鎖機(jī)制,酌情使用才是正理赊舶!
????????更進(jìn)一步說睁搭,其實使用volatile的目的是為了防止暴露一個未初始化的不完整單例實例,導(dǎo)致系統(tǒng)崩潰笼平。因為創(chuàng)建單例實例其實需要經(jīng)過以下幾步:首先分配內(nèi)存空間园骆、然后將內(nèi)存空間的首地址指向引用(指針),最后調(diào)用構(gòu)造器創(chuàng)建實例寓调,由于在第二步的時候這個引用(指針)就會變的非null锌唾,那么在第三步未執(zhí)行,真正的單例實例還未創(chuàng)建完成的時候夺英,一個線程過來在第一個校驗中為false晌涕,將會直接將不完整的實例返回,從而造成系統(tǒng)崩潰痛悯。
2.4 類級內(nèi)部類方式
????????餓漢式會占用較多的空間余黎,因為其在類加載時就會完成實例化,而懶漢式又存在執(zhí)行速率慢的情況载萌,雙重加鎖機(jī)制呢惧财?又有執(zhí)行效率差的毛病巡扇,有沒有一種完美的方式可以規(guī)避這些毛病呢?
????????貌似有的垮衷,就是使用類級內(nèi)部類結(jié)合多線程默認(rèn)同步鎖厅翔,同時實現(xiàn)延遲加載和線程安全。
public class ClassInnerClassDanli {
public static class DanliHolder{
private static ClassInnerClassDanli dl = new ClassInnerClassDanli();
}
private ClassInnerClassDanli(){}
public static ClassInnerClassDanli getInstance(){
return DanliHolder.dl;
}
}
????????如上代碼搀突,所謂類級內(nèi)部類刀闷,就是靜態(tài)內(nèi)部類,這種內(nèi)部類與其外部類之間并沒有從屬關(guān)系仰迁,加載外部類的時候涩赢,并不會同時加載其靜態(tài)內(nèi)部類,只有在發(fā)生調(diào)用的時候才會進(jìn)行加載轩勘,加載的時候就會創(chuàng)建單例實例并返回,有效實現(xiàn)了懶加載(延遲加載)怯邪,至于同步問題绊寻,我們采用和餓漢式同樣的靜態(tài)初始化器的方式,借助JVM來實現(xiàn)線程安全悬秉。
????????其實使用靜態(tài)初始化器的方式會在類加載時創(chuàng)建類的實例澄步,但是我們將實例的創(chuàng)建顯式放置在靜態(tài)內(nèi)部類中,它會導(dǎo)致在外部類加載時不進(jìn)行實例創(chuàng)建和泌,這樣就能實現(xiàn)我們的雙重目的:延遲加載和線程安全村缸。
四、使用
????????在Spring中創(chuàng)建的Bean實例默認(rèn)都是單例模式存在的武氓。