單例模式(Singleton)
單例對(duì)象(Singleton)是一種常用的設(shè)計(jì)模式嫉父。在Java應(yīng)用中,單例對(duì)象能保證在一個(gè)JVM中,該對(duì)象只有一個(gè)實(shí)例存在犹菱。
這樣的模式有幾個(gè)好處:
某些類創(chuàng)建比較頻繁,對(duì)于一些大型的對(duì)象吮炕,這是一筆很大的系統(tǒng)開銷腊脱。
省去了new操作符,降低了系統(tǒng)內(nèi)存的使用頻率龙亲,減輕GC壓力陕凹。
有些類如交易所的核心交易引擎,控制著交易流程鳄炉,如果該類可以創(chuàng)建多個(gè)的話杜耙,系統(tǒng)完全亂了。(比如一個(gè)軍隊(duì)出現(xiàn)了多個(gè)司令員同時(shí)指揮拂盯,肯定會(huì)亂成一團(tuán))佑女,所以只有使用單例模式,才能保證核心交易服務(wù)器獨(dú)立控制整個(gè)流程谈竿。
首先我們寫一個(gè)簡(jiǎn)單的單例類:
public class Singleton {
/* 持有私有靜態(tài)實(shí)例团驱,防止被引用,此處賦值為null空凸,目的是實(shí)現(xiàn)延遲加載 */
private static Singleton instance = null;
/* 私有構(gòu)造方法嚎花,防止被實(shí)例化 */
private Singleton() {
}
/* 靜態(tài)工程方法,創(chuàng)建實(shí)例 */
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
/* 如果該對(duì)象被用于序列化劫恒,可以保證對(duì)象在序列化前后保持一致 */
public Object readResolve() {
return instance;
}
}
這個(gè)類可以滿足基本要求贩幻,但是,像這樣毫無線程安全保護(hù)的類两嘴,如果我們把它放入多線程的環(huán)境下丛楚,肯定就會(huì)出現(xiàn)問題了,如何解決憔辫?
我們首先會(huì)想到對(duì)getInstance方法加synchronized關(guān)鍵字趣些,如下:
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
但是,synchronized關(guān)鍵字鎖住的是這個(gè)對(duì)象贰您,這樣的用法坏平,在性能上會(huì)有所下降拢操,因?yàn)槊看握{(diào)用getInstance(),都要對(duì)對(duì)象上鎖舶替,事實(shí)上令境,只有在第一次創(chuàng)建對(duì)象的時(shí)候需要加鎖,之后就不需要了顾瞪,所以舔庶,這個(gè)地方需要改進(jìn)。我們改成下面這個(gè):
public static Singleton getInstance() {
if (instance == null) {
synchronized (instance) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
似乎解決了之前提到的問題陈醒,將synchronized關(guān)鍵字加在了內(nèi)部惕橙,也就是說當(dāng)調(diào)用的時(shí)候是不需要加鎖的,只有在instance為null钉跷,并創(chuàng)建對(duì)象的時(shí)候才需要加鎖弥鹦,性能有一定的提升。
但是爷辙,這樣的情況彬坏,還是有可能有問題的,看下面的情況:
在Java指令中創(chuàng)建對(duì)象和賦值操作是分開進(jìn)行的犬钢,也就是說instance = new Singleton();語(yǔ)句是分兩步執(zhí)行的苍鲜。但是JVM并不保證這兩個(gè)操作的先后順序,也就是說有可能JVM會(huì)為新的Singleton實(shí)例分配空間玷犹,然后直接賦值給instance成員混滔,然后再去初始化這個(gè)Singleton實(shí)例。這樣就可能出錯(cuò)了歹颓,我們以A坯屿、B兩個(gè)線程為例:
A、B線程同時(shí)進(jìn)入了第一個(gè)if判斷
A首先進(jìn)入synchronized塊巍扛,由于instance為null领跛,所以它執(zhí)行instance = new Singleton();
由于JVM內(nèi)部的優(yōu)化機(jī)制,JVM先畫出了一些分配給Singleton實(shí)例的空白內(nèi)存撤奸,并賦值給instance成員(注意此時(shí)JVM沒有開始初始化這個(gè)實(shí)例)吠昭,然后A離開了synchronized塊。
B進(jìn)入synchronized塊胧瓜,由于instance此時(shí)不是null矢棚,因此它馬上離開了synchronized塊并將結(jié)果返回給調(diào)用該方法的程序。
此時(shí)B線程打算使用Singleton實(shí)例府喳,卻發(fā)現(xiàn)它沒有被初始化蒲肋,于是錯(cuò)誤發(fā)生了。
所以程序還是有可能發(fā)生錯(cuò)誤,其實(shí)程序在運(yùn)行過程是很復(fù)雜的兜粘,從這點(diǎn)我們就可以看出申窘,尤其是在寫多線程環(huán)境下的程序更有難度,有挑戰(zhàn)性孔轴。
我們對(duì)該程序做進(jìn)一步優(yōu)化:
private static class SingletonFactory{
private static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonFactory.instance;
}
實(shí)際情況是剃法,單例模式使用內(nèi)部類來維護(hù)單例的實(shí)現(xiàn),JVM內(nèi)部的機(jī)制能夠保證當(dāng)一個(gè)類被加載的時(shí)候距糖,這個(gè)類的加載過程是線程互斥的玄窝。這樣當(dāng)我們第一次調(diào)用getInstance的時(shí)候,JVM能夠幫我們保證instance只被創(chuàng)建一次悍引,并且會(huì)保證把賦值給instance的內(nèi)存初始化完畢,這樣我們就不用擔(dān)心上面的問題帽氓。同時(shí)該方法也只會(huì)在第一次調(diào)用的時(shí)候使用互斥機(jī)制趣斤,這樣就解決了低性能問題。
這樣我們暫時(shí)總結(jié)一個(gè)完美的單例模式:
public class Singleton {
/* 私有構(gòu)造方法黎休,防止被實(shí)例化 */
private Singleton() {
}
/* 此處使用一個(gè)內(nèi)部類來維護(hù)單例 */
private static class SingletonFactory {
private static Singleton instance = new Singleton();
}
/* 獲取實(shí)例 */
public static Singleton getInstance() {
return SingletonFactory.instance;
}
/* 如果該對(duì)象被用于序列化浓领,可以保證對(duì)象在序列化前后保持一致 */
public Object readResolve() {
return getInstance();
}
}
其實(shí)說它完美,也不一定势腮,如果在構(gòu)造函數(shù)中拋出異常联贩,實(shí)例將永遠(yuǎn)得不到創(chuàng)建,也會(huì)出錯(cuò)捎拯。所以說泪幌,十分完美的東西是沒有的,我們只能根據(jù)實(shí)際情況署照,選擇最適合自己應(yīng)用場(chǎng)景的實(shí)現(xiàn)方法祸泪。也有人這樣實(shí)現(xiàn):因?yàn)槲覀冎恍枰趧?chuàng)建類的時(shí)候進(jìn)行同步,所以只要將創(chuàng)建和getInstance()分開建芙,單獨(dú)為創(chuàng)建加synchronized關(guān)鍵字没隘,也是可以的:
public class SingletonTest {
private static SingletonTest instance = null;
private SingletonTest() {
}
private static synchronized void syncInit() {
if (instance == null) {
instance = new SingletonTest();
}
}
public static SingletonTest getInstance() {
if (instance == null) {
syncInit();
}
return instance;
}
}
考慮性能的話,整個(gè)程序只需創(chuàng)建一次實(shí)例禁荸,所以性能也不會(huì)有什么影響右蒲。
補(bǔ)充:采用"影子實(shí)例"的辦法為單例對(duì)象的屬性同步更新
public class SingletonTest {
private static SingletonTest instance = null;
private Vector properties = null;
public Vector getProperties() {
return properties;
}
private SingletonTest() {
}
private static synchronized void syncInit() {
if (instance == null) {
instance = new SingletonTest();
}
}
public static SingletonTest getInstance() {
if (instance == null) {
syncInit();
}
return instance;
}
public void updateProperties() {
SingletonTest shadow = new SingletonTest();
properties = shadow.getProperties();
}
}
通過單例模式的學(xué)習(xí)告訴我們:
單例模式理解起來簡(jiǎn)單,但是具體實(shí)現(xiàn)起來還是有一定的難度赶熟。
synchronized關(guān)鍵字鎖定的是對(duì)象瑰妄,在用的時(shí)候,一定要在恰當(dāng)?shù)牡胤绞褂茫ㄗ⒁庑枰褂面i的對(duì)象和過程钧大,可能有的時(shí)候并不是整個(gè)對(duì)象及整個(gè)過程都需要鎖)翰撑。
到這兒,單例模式基本已經(jīng)講完了,結(jié)尾處眶诈,筆者突然想到另一個(gè)問題涨醋,就是采用類的靜態(tài)方法,實(shí)現(xiàn)單例模式的效果逝撬,也是可行的浴骂,此處二者有什么不同?
首先宪潮,靜態(tài)類不能實(shí)現(xiàn)接口溯警。(從類的角度說是可以的,但是那樣就破壞了靜態(tài)了狡相。因?yàn)榻涌谥胁辉试S有static修飾的方法梯轻,所以即使實(shí)現(xiàn)了也是非靜態(tài)的)
其次,單例可以被延遲初始化尽棕,靜態(tài)類一般在第一次加載是初始化喳挑。之所以延遲加載,是因?yàn)橛行╊惐容^龐大滔悉,所以延遲加載有助于提升性能伊诵。
再次,單例類可以被繼承回官,他的方法可以被覆寫曹宴。但是靜態(tài)類內(nèi)部方法都是static,無法被覆寫歉提。
最后一點(diǎn)笛坦,單例類比較靈活,畢竟從實(shí)現(xiàn)上只是一個(gè)普通的Java類唯袄,只要滿足單例的基本需求弯屈,你可以在里面隨心所欲的實(shí)現(xiàn)一些其它功能,但是靜態(tài)類不行恋拷。
從上面這些概括中资厉,基本可以看出二者的區(qū)別,但是蔬顾,從另一方面講宴偿,我們上面最后實(shí)現(xiàn)的那個(gè)單例模式,內(nèi)部就是用一個(gè)靜態(tài)類來實(shí)現(xiàn)的诀豁,所以窄刘,二者有很大的關(guān)聯(lián),只是我們考慮問題的層面不同罷了舷胜。兩種思想的結(jié)合娩践,才能造就出完美的解決方案,就像HashMap采用數(shù)組+鏈表來實(shí)現(xiàn)一樣,其實(shí)生活中很多事情都是這樣翻伺,單用不同的方法來處理問題材泄,總是有優(yōu)點(diǎn)也有缺點(diǎn),最完美的方法是吨岭,結(jié)合各個(gè)方法的優(yōu)點(diǎn)拉宗,才能最好的解決問題!