前言
??在GoF的23種設(shè)計(jì)模式中,單例模式是比較簡單的一種。然而,有時(shí)候越是簡單的東西越容易出現(xiàn)問題。下面就單例設(shè)計(jì)模式詳細(xì)的探討一下。
??所謂單例模式,簡單來說,就是在整個(gè)應(yīng)用中保證類只有一個(gè)實(shí)例存在。這個(gè)類的實(shí)例只提供了一個(gè)全局變量,用處相當(dāng)廣泛,比如保存全局?jǐn)?shù)據(jù),實(shí)現(xiàn)全局性的操作等。
分析
??1. 最簡單的實(shí)現(xiàn) —— 餓漢式
??首先,能夠想到的最簡單的實(shí)現(xiàn)是,把類的構(gòu)造函數(shù)寫成private的,從而保證別的類不能實(shí)例化此類,然后在類中提供一個(gè)靜態(tài)的實(shí)例并能夠返回給使用者。這樣,使用者就可以通過這個(gè)引用使用到這個(gè)類的實(shí)例了。
public class SingletonClass {
private static SingletonClass instance = new SingletonClass();
public static SingletonClass getInstance() {
return instance;
}
private SingletonClass() {
}
}
外部使用者如果需要使用SingletonClass的實(shí)例,只能通過getInstance()方法,并且它的構(gòu)造方法是private的,這樣就保證了只能有一個(gè)對象存在。
??2. 性能優(yōu)化 —— lazy loaded 懶漢式
??上面的代碼雖然簡單,但是有一個(gè)問題——無論這個(gè)類是否被使用,都會(huì)創(chuàng)建一個(gè)instance對象。如果這個(gè)創(chuàng)建過程很耗時(shí),比如需要連接10000次jdbc實(shí)例連接或者10000多個(gè)模版實(shí)例,并且這個(gè)類還并不一定會(huì)被使用,那么這個(gè)創(chuàng)建過程就是無用的。
??為了解決這個(gè)問題,我們想到了新的解決方案:
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í)候通過判斷是否為null來創(chuàng)建對象。
??我們來想象一下這個(gè)過程。要使用SingletonClass,調(diào)用getInstance()方法。第一次的時(shí)候發(fā)現(xiàn)instance是null,然后就新建一個(gè)對象,返回出去;第二次再使用的時(shí)候,因?yàn)檫@個(gè)instance是static的,所以已經(jīng)不是null了,因此不會(huì)再創(chuàng)建對象,直接將其返回。
這個(gè)過程就成為lazy loaded,也就是延遲加載——直到使用的時(shí)候才進(jìn)行加載。
??3. 同步
??上面的代碼很清楚,也很簡單。然而就像那句名言:“80%的錯(cuò)誤都是由20%代碼優(yōu)化引起的”。單線程下,這段代碼沒有什么問題,可是如果是多線程,麻煩就來了。我們來分析一下:
??線程1希望使用SingletonClass,調(diào)用getInstance()方法。因?yàn)槭堑谝淮握{(diào)用,1就發(fā)現(xiàn)instance是null的,于是它開始創(chuàng)建實(shí)例,就在這個(gè)時(shí)候,CPU發(fā)生時(shí)間片切換(或者被搶奪執(zhí)行),線程2開始執(zhí)行,它要使用SingletonClass,調(diào)用getInstance()方法,同樣檢測到instance是null——注意,這是在1檢測完之后切換的,也就是說1并沒有來得及創(chuàng)建對象——因此2開始創(chuàng)建。2創(chuàng)建完成后,cpu切換到1繼續(xù)執(zhí)行,因?yàn)樗呀?jīng)檢測完了,所以1不會(huì)再檢測一遍,它會(huì)直接創(chuàng)建對象。這樣,線程1和2各自擁有一個(gè)SingletonClass的對象——單例失??!
??解決的方法也很簡單,那就是加鎖:
public class SingletonClass {
private static SingletonClass instance = null;
public synchronized static SingletonClass getInstance() {
if(instance == null) {
instance = new SingletonClass();
}
return instance;
}
private SingletonClass() {
}
}
??4. 又是性能問題
??上面的代碼又是很清楚很簡單的,然而,簡單的東西往往不夠理想。理想的東西往往不夠簡單,這就是生活。這段代碼毫無疑問存在性能的問題——synchronized修飾的同步塊可是要比一般的代碼段慢上幾倍的!如果存在很多次getInstance()的調(diào)用,那性能問題就不得不考慮了!
??讓我們來分析一下,究竟是整個(gè)方法都必須加鎖,還是僅僅其中某一句加鎖就足夠了?我們?yōu)槭裁匆渔i呢?分析一下出現(xiàn)lazy loaded的那種情形的原因。原因就是檢測null的操作和創(chuàng)建對象的操作分離了。如果這兩個(gè)操作能夠原子地進(jìn)行,那么單例就已經(jīng)保證了。于是,我們開始修改代碼:
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() {
}
}
還有問題嗎?首先判斷instance是不是為null,如果為null,加鎖初始化;如果不為null,直接返回instance。
這就是double-checked locking設(shè)計(jì)實(shí)現(xiàn)單例模式。但是還有問題。。。。。。
??5. JMM中
??在Java虛擬機(jī)規(guī)范中試圖定義一種Java內(nèi)存模型(Java Memory Model,JMM)來屏蔽各個(gè)硬件平臺(tái)和操作系統(tǒng)的內(nèi)存訪問差異,以實(shí)現(xiàn)讓Java程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪問效果。那么Java內(nèi)存模型規(guī)定了哪些東西呢,它定義了程序中變量的訪問規(guī)則,往大一點(diǎn)說是定義了程序執(zhí)行的次序。注意,為了獲得較好的執(zhí)行性能,Java內(nèi)存模型并沒有限制執(zhí)行引擎使用處理器的寄存器或者高速緩存來提升指令執(zhí)行速度,也沒有限制編譯器對指令進(jìn)行重排序。也就是說,在java內(nèi)存模型中,也會(huì)存在緩存一致性問題和指令重排序的問題。
??下面來想一下,創(chuàng)建一個(gè)變量需要哪些步驟呢?一個(gè)是申請一塊內(nèi)存,調(diào)用構(gòu)造方法進(jìn)行初始化操作,另一個(gè)是分配一個(gè)指針指向這塊內(nèi)存。這兩個(gè)操作誰在前誰在后呢?JMM規(guī)范并沒有規(guī)定。(可能重排序)那么就存在這么一種情況,JVM是先開辟出一塊內(nèi)存,然后把指針指向這塊內(nèi)存,最后調(diào)用構(gòu)造方法進(jìn)行初始化。
??線程1開始創(chuàng)建SingletonClass的實(shí)例,此時(shí)線程2調(diào)用了getInstance()方法,首先判斷instance是否為null。按照我們上面所說的內(nèi)存模型,1已經(jīng)把instance指向了那塊內(nèi)存,只是還沒有調(diào)用構(gòu)造方法,因此2檢測到instance不為null,于是直接把instance返回了——問題出現(xiàn)了,盡管instance不為null,但它并沒有構(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)鍵字有了明確的語義——在JDK1.5之前,volatile是個(gè)關(guān)鍵字,但是并沒有明確的規(guī)定其用途——被volatile修飾的寫變量不能和之前的讀寫代碼調(diào)整,讀變量不能和之后的讀寫代碼調(diào)整!因此,只要我們簡單的把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() {
}
}