單例模式應(yīng)該是日常開發(fā)中用得最多的設(shè)計模式了,它的思想就是保證在應(yīng)用中一個類的實例只能有一個。
什么情形下需要用到單例模式?
在程序中我們經(jīng)常會遇到有類似配置文件的需求,一般在整個應(yīng)用中配置信息應(yīng)該都是需要共享同一份的,這時可以利用單例模式,保證在程序中用到此配置類的實例時,都是同一個實例,保證程序運(yùn)行的正確。
類似于這種情形還有很多,比如數(shù)據(jù)庫,線程池等。針對這種共享的情形有人可能就會有疑問,那我把這些配置信息都設(shè)置為靜態(tài)的成員變量不就得了,當(dāng)然,這樣做理論上沒問題,也能保證程序中共享一份信息,但是靜態(tài)變量的生命周期是跟隨類的,比普通成員變量要長,所以對內(nèi)存是很大的消耗,這也是單例模式最大的優(yōu)點,能節(jié)省內(nèi)存。
單例模式的幾種寫法
餓漢式:
public class SingleInstance {
private static SingleInstance sInstance = new SingleInstance();
private SingleInstance(){}
public static SingleInstance getInstance(){
return sInstance;
}
}
可以看出這種寫法比較暴力,在類加載的時候就初始化創(chuàng)建了一個實例,不管你需不需要使用都給你創(chuàng)建好了,所以稱為餓漢式。這種寫法的缺點就是不是懶加載,就算你不需要他也創(chuàng)建了,所以造成了一定的內(nèi)存浪費(fèi)。這種方式是線程安全的,可以正常使用,但不推薦
懶漢式1(線程不安全)
public class SingleInstance {
private static SingleInstance sInstance;
private SingleInstance(){}
public static SingleInstance getInstance(){
if(sInstance==null){
sInstance = new SingleInstance();
}
return sInstance;
}
}
這種方式是當(dāng)你需要這個實例調(diào)用getInstance()的時候才會去創(chuàng)建,所以稱為懶漢式。這種寫法看起來是解決了餓漢式浪費(fèi)內(nèi)存的情況,但這種寫法是線程不安全的
假設(shè)有一個線程調(diào)用了getInstance() 方法,判斷sInstance等于null之后讓出了cpu的執(zhí)行權(quán),此時另外一個線程拿到了cpu的執(zhí)行權(quán),而且也進(jìn)入getInstance() 方法后判斷sInstance==null也還是true,最終這兩個線程就會創(chuàng)建出兩個實例,是線程不安全的,在多線程中不可以使用。
既然存在線程不安全問題,那么就加個鎖試試
懶漢式2(效率低)
public class SingleInstance {
private static SingleInstance sInstance;
private SingleInstance(){}
public static synchronized SingleInstance getInstance(){
if(sInstance==null){
sInstance = new SingleInstance();
}
return sInstance;
}
}
相對于前一種,在getInstance() 方法上加了一個鎖,用來保證線程安全,但是這中寫法效率太低。
假設(shè)線程A拿到了鎖,進(jìn)入了getInstance() 方法,但是方法還沒執(zhí)行完就讓出了cpu執(zhí)行權(quán),此時另外一個線程也需要調(diào)用getInstance(),發(fā)現(xiàn)鎖還沒釋放,于是就無法繼續(xù)執(zhí)行,得等線程A釋放鎖。但實際上,我們只需要保證在創(chuàng)建實例的時候線程安全就可以了,創(chuàng)建好之后判斷sInstance==null為false,直接return就行,不會存在線程安全的問題,所以這種寫法降低了程序執(zhí)行的效率,不推薦使用。
既然這種效率低,是由于鎖的范圍太大,那換一個鎖的位置試試
懶漢式3(線程不安全)
public class SingleInstance {
private static SingleInstance sInstance;
private SingleInstance(){}
public static SingleInstance getInstance(){
if(sInstance==null){
synchronized(SingleInstance.class){
sInstance = new SingleInstance();
}
}
return sInstance;
}
}
這種寫法在實例已經(jīng)創(chuàng)建好的情況下,會直接返回對象,不用等待鎖,提高了運(yùn)行效率。但是仍然存在線程不安全問題,當(dāng)實例還沒創(chuàng)建的情況下,同懶漢式1一樣,當(dāng)多個線程都判斷if(sInstance==null)為true的情況,都會進(jìn)入鎖住的代碼塊內(nèi),最終創(chuàng)建出多個實例。因此這種寫法也不可以使用
懶漢式4(雙重校驗鎖DCL,推薦)
public class SingleInstance {簡潔
private static volatile SingleInstance sInstance;
private SingleInstance(){}
public static SingleInstance getInstance(){
if(sInstance==null){
synchronized(SingleInstance.class){
if(sInstance==null){
sInstance = new SingleInstance();
}
}
}
return sInstance;
}
}
分析懶漢式3,是因為當(dāng)多個線程都能進(jìn)入同步代碼塊時,會創(chuàng)建多個對象。所以改為在進(jìn)入同步代碼塊之后,再判斷一次,這樣即使都先后進(jìn)入了同步代碼塊,也不會多創(chuàng)建實例了,這樣既解決了線程安全的問題,也解決了效率低的問題。這種雙重校驗的寫法應(yīng)該是平時用的最多的一種。
在雙重校驗鎖方式中,還有個重要的地方做了改動,在聲明sInstance時加了volatile關(guān)鍵字,之所以加入這個關(guān)鍵字是因為sInstance = new SingleInstance()這一行代碼,不是原子操作。
在jvm中,這一行代碼并不是一個操作完成的,而是大概會分成三個步驟去執(zhí)行
- 給sInstance分配內(nèi)存
- new操作,創(chuàng)建對象
- 將對象指向內(nèi)存空間
其中第三步執(zhí)行之后sInstance就不等于null了,由于還存在指令重排優(yōu)化,可能會先執(zhí)行第3步,再執(zhí)行第2部,假設(shè)某個線程先執(zhí)行了第3步,還沒執(zhí)行第二步,讓出了cpu的執(zhí)行權(quán),此時另外一個線程判斷sIntance已經(jīng)不為null,則直接返回sIntance,但實際上sInstance還并沒有創(chuàng)建真正的對象,最終就會導(dǎo)致程序出錯。
總結(jié)一下這個問題的根源就是某個線程對sInstance的寫操作還沒完成,存在一個中間狀態(tài),此時讓另外一個線程拿去進(jìn)行了讀操作,導(dǎo)致出現(xiàn)問題。
解決這個問題的辦法就是使用volatile關(guān)鍵字,volatile會禁止指令重排,會保證在上述的三個操作執(zhí)行完之后,才會讓另外一個線程進(jìn)行讀操作,保障sInstance不會出現(xiàn)中間狀態(tài)被讀而產(chǎn)生錯誤的情況。
使用內(nèi)部類
public class SingleInstance {
private SingleInstance(){}
private static class SingleInstanceHolder{
private static SingleInstance sInstance = new SingleInstance();
}
public static SingleInstance getInstance(){
return SingleInstanceHolder.sInstance;
}
}
這種寫法個人感覺就比較高級了,它主要利用了內(nèi)部類的加載時機(jī)以及ClassLoader的同步機(jī)制保證單例的實現(xiàn)。
這種寫法看起來類似于餓漢式的寫法,但其實內(nèi)部類要在外部類調(diào)用getInstance()方法使用到它時才會去加載,于是就變成了懶加載模式。其次ClassLoader在加載類的時候會保證只有一個線程來加載,也不會出現(xiàn)線程不安全的問題
使用枚舉
public enum SingleInstance {
sInstance;
}
這種方式寫起來就相當(dāng)暴力了,使用也是直接用SingleInstance.sInstance就可以。
根據(jù)枚舉的特性,因為sInstance是SingleInstance的一個實例,而且只定義了一個sInstance,sInstance也不能被克隆,所以就保證了單例,同時因為創(chuàng)建枚舉的過程是線程安全的,所以在多線程中使用也沒有問題
另外枚舉自身已經(jīng)處理了序列化的問題,不會因為反序列化和反射產(chǎn)生多個實例的情況(這一塊說實話還不是很了解原理,感覺也不是幾句話能說清楚的,所以等深入研究后再做補(bǔ)充)
總結(jié)一下,在日常開發(fā)中用的比較多的應(yīng)該就是雙重校驗鎖的方式了,但是現(xiàn)在大牛們更推薦的是靜態(tài)內(nèi)部類和枚舉的方式,特別是枚舉,代碼簡潔,還能解決反序列化和反射引起的問題
以上就是對單例設(shè)計模式的一些理解和總結(jié),如有不對的地方歡迎批評指正