前言
??在GoF的23種設(shè)計(jì)模式中崎场,單例模式是比較簡(jiǎn)單的一種佩耳。然而,有時(shí)候越是簡(jiǎn)單的東西越容易出現(xiàn)問(wèn)題照雁。下面就單例設(shè)計(jì)模式詳細(xì)的探討一下蚕愤。
??所謂單例模式答恶,簡(jiǎn)單來(lái)說(shuō)饺蚊,就是在整個(gè)應(yīng)用中保證類只有一個(gè)實(shí)例存在。這個(gè)類的實(shí)例只提供了一個(gè)全局變量悬嗓,用處相當(dāng)廣泛污呼,比如保存全局?jǐn)?shù)據(jù),實(shí)現(xiàn)全局性的操作等包竹。
分析
??1. 最簡(jiǎn)單的實(shí)現(xiàn) —— 餓漢式
??首先燕酷,能夠想到的最簡(jiǎn)單的實(shí)現(xiàn)是,把類的構(gòu)造函數(shù)寫成private的周瞎,從而保證別的類不能實(shí)例化此類苗缩,然后在類中提供一個(gè)靜態(tài)的實(shí)例并能夠返回給使用者。這樣声诸,使用者就可以通過(guò)這個(gè)引用使用到這個(gè)類的實(shí)例了酱讶。
public class SingletonClass {
private static SingletonClass instance = new SingletonClass();
public static SingletonClass getInstance() {
return instance;
}
private SingletonClass() {
}
}
外部使用者如果需要使用SingletonClass的實(shí)例,只能通過(guò)getInstance()方法彼乌,并且它的構(gòu)造方法是private的泻肯,這樣就保證了只能有一個(gè)對(duì)象存在渊迁。
??2. 性能優(yōu)化 —— lazy loaded 懶漢式
??上面的代碼雖然簡(jiǎn)單,但是有一個(gè)問(wèn)題——無(wú)論這個(gè)類是否被使用灶挟,都會(huì)創(chuàng)建一個(gè)instance對(duì)象琉朽。如果這個(gè)創(chuàng)建過(guò)程很耗時(shí),比如需要連接10000次jdbc實(shí)例連接或者10000多個(gè)模版實(shí)例稚铣,并且這個(gè)類還并不一定會(huì)被使用箱叁,那么這個(gè)創(chuàng)建過(guò)程就是無(wú)用的。
??為了解決這個(gè)問(wèn)題惕医,我們想到了新的解決方案:
public class SingletonClass {
private static SingletonClass instance = null;
public static SingletonClass getInstance() {
if(instance == null) {
instance = new SingletonClass();
}
return instance;
}
private SingletonClass() {
}
}
??代碼的變化有1處——把instance初始化為null蝌蹂,直到第一次使用的時(shí)候通過(guò)判斷是否為null來(lái)創(chuàng)建對(duì)象。
??我們來(lái)想象一下這個(gè)過(guò)程曹锨。要使用SingletonClass孤个,調(diào)用getInstance()方法。第一次的時(shí)候發(fā)現(xiàn)instance是null沛简,然后就新建一個(gè)對(duì)象齐鲤,返回出去;第二次再使用的時(shí)候椒楣,因?yàn)檫@個(gè)instance是static的给郊,所以已經(jīng)不是null了,因此不會(huì)再創(chuàng)建對(duì)象捧灰,直接將其返回淆九。
這個(gè)過(guò)程就成為lazy loaded,也就是延遲加載——直到使用的時(shí)候才進(jìn)行加載毛俏。
??3. 同步
??上面的代碼很清楚炭庙,也很簡(jiǎn)單。然而就像那句名言:“80%的錯(cuò)誤都是由20%代碼優(yōu)化引起的”煌寇。單線程下焕蹄,這段代碼沒(méi)有什么問(wèn)題,可是如果是多線程阀溶,麻煩就來(lái)了腻脏。我們來(lái)分析一下:
??線程1希望使用SingletonClass,調(diào)用getInstance()方法银锻。因?yàn)槭堑谝淮握{(diào)用永品,1就發(fā)現(xiàn)instance是null的,于是它開(kāi)始創(chuàng)建實(shí)例击纬,就在這個(gè)時(shí)候鼎姐,CPU發(fā)生時(shí)間片切換(或者被搶奪執(zhí)行),線程2開(kāi)始執(zhí)行,它要使用SingletonClass症见,調(diào)用getInstance()方法喂走,同樣檢測(cè)到instance是null——注意,這是在1檢測(cè)完之后切換的谋作,也就是說(shuō)1并沒(méi)有來(lái)得及創(chuàng)建對(duì)象——因此2開(kāi)始創(chuàng)建芋肠。2創(chuàng)建完成后,cpu切換到1繼續(xù)執(zhí)行遵蚜,因?yàn)樗呀?jīng)檢測(cè)完了帖池,所以1不會(huì)再檢測(cè)一遍,它會(huì)直接創(chuàng)建對(duì)象吭净。這樣睡汹,線程1和2各自擁有一個(gè)SingletonClass的對(duì)象——單例失敗寂殉!
??解決的方法也很簡(jiǎn)單囚巴,那就是加鎖:
public class SingletonClass {
private static SingletonClass instance = null;
public synchronized static SingletonClass getInstance() {
if(instance == null) {
instance = new SingletonClass();
}
return instance;
}
private SingletonClass() {
}
}
??4. 又是性能問(wèn)題
??上面的代碼又是很清楚很簡(jiǎn)單的,然而友扰,簡(jiǎn)單的東西往往不夠理想彤叉。理想的東西往往不夠簡(jiǎn)單,這就是生活村怪。這段代碼毫無(wú)疑問(wèn)存在性能的問(wèn)題——synchronized修飾的同步塊可是要比一般的代碼段慢上幾倍的秽浇!如果存在很多次getInstance()的調(diào)用,那性能問(wèn)題就不得不考慮了甚负!
??讓我們來(lái)分析一下柬焕,究竟是整個(gè)方法都必須加鎖,還是僅僅其中某一句加鎖就足夠了梭域?我們?yōu)槭裁匆渔i呢斑举?分析一下出現(xiàn)lazy loaded的那種情形的原因。原因就是檢測(cè)null的操作和創(chuàng)建對(duì)象的操作分離了碰辅。如果這兩個(gè)操作能夠原子地進(jìn)行懂昂,那么單例就已經(jīng)保證了介时。于是没宾,我們開(kāi)始修改代碼:
public class SingletonClass {
private static SingletonClass instance = null;
public static SingletonClass getInstance() {
if (instance == null) {
synchronized (SingletonClass.class) {
if (instance == null) {
instance = new SingletonClass();
}
}
}
return instance;
}
private SingletonClass() {
}
}
還有問(wèn)題嗎?首先判斷instance是不是為null沸柔,如果為null循衰,加鎖初始化;如果不為null褐澎,直接返回instance会钝。
這就是double-checked locking設(shè)計(jì)實(shí)現(xiàn)單例模式。但是還有問(wèn)題。迁酸。先鱼。。奸鬓。焙畔。
??5. JMM中
??在Java虛擬機(jī)規(guī)范中試圖定義一種Java內(nèi)存模型(Java Memory Model,JMM)來(lái)屏蔽各個(gè)硬件平臺(tái)和操作系統(tǒng)的內(nèi)存訪問(wèn)差異串远,以實(shí)現(xiàn)讓Java程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪問(wèn)效果宏多。那么Java內(nèi)存模型規(guī)定了哪些東西呢,它定義了程序中變量的訪問(wèn)規(guī)則澡罚,往大一點(diǎn)說(shuō)是定義了程序執(zhí)行的次序伸但。注意,為了獲得較好的執(zhí)行性能留搔,Java內(nèi)存模型并沒(méi)有限制執(zhí)行引擎使用處理器的寄存器或者高速緩存來(lái)提升指令執(zhí)行速度更胖,也沒(méi)有限制編譯器對(duì)指令進(jìn)行重排序。也就是說(shuō)隔显,在java內(nèi)存模型中函喉,也會(huì)存在緩存一致性問(wèn)題和指令重排序的問(wèn)題。
??下面來(lái)想一下荣月,創(chuàng)建一個(gè)變量需要哪些步驟呢管呵?一個(gè)是申請(qǐng)一塊內(nèi)存,調(diào)用構(gòu)造方法進(jìn)行初始化操作哺窄,另一個(gè)是分配一個(gè)指針指向這塊內(nèi)存捐下。這兩個(gè)操作誰(shuí)在前誰(shuí)在后呢?JMM規(guī)范并沒(méi)有規(guī)定萌业。(可能重排序)那么就存在這么一種情況坷襟,JVM是先開(kāi)辟出一塊內(nèi)存,然后把指針指向這塊內(nèi)存生年,最后調(diào)用構(gòu)造方法進(jìn)行初始化婴程。
??線程1開(kāi)始創(chuàng)建SingletonClass的實(shí)例,此時(shí)線程2調(diào)用了getInstance()方法抱婉,首先判斷instance是否為null档叔。按照我們上面所說(shuō)的內(nèi)存模型,1已經(jīng)把instance指向了那塊內(nèi)存蒸绩,只是還沒(méi)有調(diào)用構(gòu)造方法衙四,因此2檢測(cè)到instance不為null,于是直接把instance返回了——問(wèn)題出現(xiàn)了患亿,盡管instance不為null扎瓶,但它并沒(méi)有構(gòu)造完成,就像一套房子已經(jīng)給了你鑰匙祭阀,但你并不能住進(jìn)去,因?yàn)槔锩孢€是毛坯房挑格。此時(shí),如果2在1將instance構(gòu)造完成之前就是用了這個(gè)實(shí)例沾歪,程序就會(huì)出現(xiàn)錯(cuò)誤了恕齐!
??5. 最終解決方案
??在JDK 5之后,Java使用了新的內(nèi)存模型瞬逊。volatile關(guān)鍵字有了明確的語(yǔ)義——在JDK1.5之前显歧,volatile是個(gè)關(guān)鍵字,但是并沒(méi)有明確的規(guī)定其用途——被volatile修飾的寫變量不能和之前的讀寫代碼調(diào)整确镊,讀變量不能和之后的讀寫代碼調(diào)整士骤!因此,只要我們簡(jiǎn)單的把instance加上volatile關(guān)鍵字就可以了蕾域。
public class SingletonClass {
private volatile static SingletonClass instance = null;
public static SingletonClass getInstance() {
if (instance == null) {
synchronized (SingletonClass.class) {
if(instance == null) {
instance = new SingletonClass();
}
}
}
return instance;
}
private SingletonClass() {
}
}