設(shè)計(jì)模式筆記01——設(shè)計(jì)原則
介紹
自行實(shí)例化并向整個(gè)系統(tǒng)提供這個(gè)實(shí)例。這個(gè)類稱為單例類
單例只有一個(gè)實(shí)例對(duì)象
單例自己創(chuàng)建自己實(shí)例,構(gòu)造函數(shù)為私有
單例提供唯一的實(shí)例給外部引用
實(shí)現(xiàn)方式
單例模式實(shí)現(xiàn)由很多種形式薯蝎,我們并不進(jìn)行列舉,直說一些比較常用的囊蓝。單例模式最常見的莫過于懶漢式和餓漢式了邪媳,這也是最簡單的兩種實(shí)現(xiàn)方式,其他的實(shí)現(xiàn)方式還有雙重檢查加鎖檬洞、Lazy initialization holder class模式狸膏、枚舉等
餓漢式
這種形式的單例最為簡單且直接。那就是用final關(guān)鍵字全局聲明的時(shí)候直接實(shí)例化添怔,然后將構(gòu)造函數(shù)進(jìn)行私有化就可以完成一個(gè)單例了湾戳。
private static final UserInfo instance = new UserInfo();
public static UserInfo getInstance() {
return instance;
}
private UserInfo() {
}
說明
這種實(shí)現(xiàn)非常簡單且直接,同時(shí)帶來的就是資源的消耗广料,無論需不需要用的到這個(gè)對(duì)象都會(huì)被實(shí)例化砾脑。
懶漢式
相對(duì)于餓漢式來說,懶漢式只有在首次需要用到單例的時(shí)候才會(huì)被實(shí)例化艾杏,如果不需要那么就不會(huì)被實(shí)例化韧衣。
private static UserInfo instance = null;
public static UserInfo getInstance() {
if (instance == null) {
instance = new UserInfo();
}
return instance;
}
private UserInfo() {
}
當(dāng)getInstance被調(diào)用的時(shí)候首先對(duì)instance對(duì)象進(jìn)行判空,隨后實(shí)例化返回instance對(duì)象购桑。
所謂懶漢和餓漢是一個(gè)形象說法畅铭,第一種由于聲明直接實(shí)例化,就像一個(gè)餓了的大漢一樣看到吃的就直接去吃勃蜘;懶漢呢就是需要時(shí)候再去獲取吃的硕噩。
說明
懶漢式相較于前面的餓漢式節(jié)省了內(nèi)存的開銷,當(dāng)需要單例對(duì)象的時(shí)候才會(huì)實(shí)例化單例對(duì)象元旬。但是對(duì)于多線程并發(fā)調(diào)用getInstance的時(shí)候榴徐,大家就會(huì)發(fā)現(xiàn)其實(shí)這時(shí)候并不能保證getInstance中只實(shí)例化了一個(gè)instance對(duì)象,后面的很多中實(shí)現(xiàn)其實(shí)也就是為了解決這個(gè)問題個(gè)出現(xiàn)的匀归。
線程安全
為了解決并發(fā)調(diào)用現(xiàn)象坑资,我們可以使用synchronized關(guān)鍵字,例如:
private static UserInfo instance = null;
public static synchronized UserInfo getInstance() {
if (instance == null) {
instance = new UserInfo();
}
return instance;
}
private UserInfo() {
}
那么這個(gè)實(shí)現(xiàn)可以防止并發(fā)請(qǐng)求的時(shí)候進(jìn)行了多次實(shí)例化穆端。但是這樣就會(huì)導(dǎo)致多個(gè)線程被同時(shí)阻擋在getInstance方法袱贮,影響線程執(zhí)行性能。
為了提高性能体啰,減少被擋線程的可能性攒巍,我們對(duì)上面的實(shí)現(xiàn)進(jìn)行了修改
private static UserInfo instance = null;
public static synchronized UserInfo getInstance() {
if (instance == null) {
synchronized (UserInfo.class){
instance = new UserInfo();
}
}
return instance;
}
private UserInfo() {
}
那么這樣就會(huì)不至于所有調(diào)用getInstance方法的線程都會(huì)被阻塞嗽仪,而且在一定程度上完成了上面的需求。但是這樣我們就會(huì)發(fā)現(xiàn)依然無法完全避免單例對(duì)象被重復(fù)實(shí)例化柒莉,原因在于首次調(diào)用getInstance發(fā)放被synchronized阻擋的線程數(shù)超過1的時(shí)候闻坚,就會(huì)導(dǎo)致額外的實(shí)例化操作。
雙重檢查加鎖
首先介紹一個(gè)關(guān)鍵字——volatile兢孝,這個(gè)關(guān)鍵字應(yīng)該不算陌生窿凤,我發(fā)現(xiàn)很多單例中都或多或少見過。
volatile關(guān)鍵字標(biāo)記的變量意味著線程共享內(nèi)存跨蟹!也就是在多線程程序中其實(shí)調(diào)用該對(duì)象是同一個(gè)內(nèi)存位置雳殊,以保證各個(gè)線程調(diào)用獲取到的是實(shí)時(shí)數(shù)據(jù)
這樣結(jié)合線程鎖配合使用,不但可以保證線程安全窗轩,而且可以保證實(shí)例的唯一性
注意:volatile可能屏蔽掉虛擬機(jī)優(yōu)化內(nèi)容夯秃,從而可能降低虛擬機(jī)運(yùn)行效率
范例
結(jié)合上面的線程并發(fā)方案,我們的實(shí)現(xiàn)可以修改為:
private static volatile UserInfo instance = null;
public static synchronized UserInfo getInstance() {
if (instance == null) {
synchronized (UserInfo.class){
if (instance == null) {
instance = new UserInfo();
}
}
}
return instance;
}
private UserInfo() {
}
其實(shí)和上面的最后實(shí)現(xiàn)沒什么不一樣痢艺,唯一的不同是多了一個(gè)關(guān)鍵字volatile的修飾仓洼。這個(gè)volatile修飾可以保證當(dāng)我們的getInstance中synchronized被擋住線程超過1的時(shí)候,線程1執(zhí)行實(shí)例化之后線程2繼續(xù)接著向下執(zhí)行腹备,這時(shí)候線程2就會(huì)獲取instance對(duì)象真實(shí)情況衬潦,然后進(jìn)行下面的程序執(zhí)行。
這種單例實(shí)現(xiàn)是目前看到比較多的植酥,因?yàn)闆]什么難度镀岛,而且比較實(shí)用。但是不太建議一個(gè)系統(tǒng)中過多使用友驮,如果項(xiàng)目中單例過多漂羊,那么也會(huì)導(dǎo)致性能缺陷,上面也提到了volatile關(guān)鍵字會(huì)屏蔽一些虛擬機(jī)的優(yōu)化方案
Lazy initialization holder class模式
這個(gè)模式綜合使用了Java的類級(jí)內(nèi)部類和多線程缺省同步鎖的知識(shí)卸留,很巧妙地同時(shí)實(shí)現(xiàn)了延遲加載和線程安全
介紹
Lazy Initialization Holder Class 模式主要是利用了java虛擬機(jī)類裝載機(jī)制走越,保證了getInstance方法調(diào)用前不會(huì)被裝載,第一次被調(diào)用之后內(nèi)部類才會(huì)被裝載耻瑟,而這時(shí)候同時(shí)會(huì)裝載類的靜態(tài)域
利用了類級(jí)內(nèi)部靜態(tài)類的加載旨指,從而延遲了內(nèi)部靜態(tài)類的裝載,即延遲了instance對(duì)象的實(shí)例化過程喳整!這是一個(gè)餓漢式的類級(jí)內(nèi)部類實(shí)現(xiàn)方式
因?yàn)閖ava裝載靜態(tài)內(nèi)容只會(huì)初始化一次谆构,因此保證了線程安全,其次也做到了延遲加載的目的
使用場景
這種實(shí)現(xiàn)方式更多的強(qiáng)調(diào)一種使用場景的可能性框都,而非單例類獨(dú)立為整個(gè)系統(tǒng)提供服務(wù)搬素。例如:一個(gè)系統(tǒng)內(nèi)部某個(gè)模塊內(nèi)部的配置項(xiàng),在這個(gè)過程中可以使用這種方式,當(dāng)該模塊未啟用的時(shí)候熬尺,其實(shí)沒有任何額外的資源開銷摸屠,但是一旦模塊被調(diào)用的時(shí)候,java虛擬機(jī)就會(huì)加載相關(guān)類粱哼,這時(shí)候連帶著靜態(tài)內(nèi)部類也被加載(有且只加載一次季二,而且這個(gè)由java虛擬機(jī)控制),這時(shí)候利用靜態(tài)內(nèi)部加載的配置內(nèi)容進(jìn)行模塊初始化等操作皂吮。
這種方式在公共庫戒傻、SDK開發(fā)中比較喜歡用到,尤其是對(duì)于平臺(tái)類的SDK開發(fā)蜂筹。原因就是一個(gè)SDK中可能存在多個(gè)模塊,而且這些模塊在沒有被初始化的時(shí)候希望不要打擾集成APP的業(yè)務(wù)和實(shí)現(xiàn)芦倒,乃至SDK某部分模塊的使用艺挪。
枚舉
使用枚舉來實(shí)現(xiàn)單實(shí)例控制會(huì)更加簡潔,而且無償?shù)靥峁┝诵蛄谢瘷C(jī)制兵扬,并由JVM從根本上提供保障麻裳,絕對(duì)防止多次實(shí)例化,是更簡潔器钟、高效津坑、安全的實(shí)現(xiàn)單例的方式
枚舉屬于引用數(shù)據(jù)類型還是基本數(shù)據(jù)類型?這個(gè)問題在java中相比很多人都說不清楚傲霸,其實(shí)我也講不明白疆瑰,但是我記得C#語言中屬于基本數(shù)據(jù)類型。
即使不是基本數(shù)據(jù)類型那么他也不會(huì)是普通引用數(shù)據(jù)類型昙啄,因?yàn)槊杜e的值是固定的穆役,而且不能夠被手動(dòng)實(shí)例化創(chuàng)建。也正是因?yàn)檫@個(gè)所以也成為了一個(gè)天然的常量裝載機(jī)制梳凛。
當(dāng)然枚舉單例用的很少耿币,因?yàn)閷?duì)象之所以被廣泛應(yīng)用就在于其操作可以依據(jù)特定條件發(fā)生變化,例如全局配置項(xiàng)韧拒。如果用了枚舉操作起來不是很方便淹接,而且很多單例是需要外界數(shù)據(jù)交互的,例如:當(dāng)前登錄賬戶叛溢,當(dāng)前設(shè)置項(xiàng)等
總結(jié)
單例模式實(shí)現(xiàn)方式很多種塑悼,無論哪一種都存在一些缺點(diǎn),依據(jù)使用場景做出選擇雇初。
日常項(xiàng)目使用比較多的還是屬于雙重檢查加鎖拢肆,在模塊比較多的時(shí)候,需要通過外部初始化固定模塊的情況下我們可以采用模塊獨(dú)立的Lazy initialization holder class模式 這種方式相對(duì)而言比較可靠,當(dāng)然也可以使用雙重檢查加鎖郭怪,但是上面也提到了java虛擬機(jī)會(huì)屏蔽一些優(yōu)化內(nèi)容支示,可能會(huì)影響性能,因此對(duì)于單例比較多的時(shí)候避免頻繁使用雙重檢查加鎖鄙才!
當(dāng)然Lazy initialization holder class模式實(shí)現(xiàn)一般相較而言比較麻煩一些颂鸿。