為什么使用單例模式?
盡可能的節(jié)約內(nèi)存空間邦邦,減少無謂的GC消耗
這些類安吁,在應用中如果有兩個或者兩個以上的實例會引起錯誤,又或者我換句話說燃辖,就是這些類鬼店,在整個應用中,同一時刻郭赐,有且只能有一種狀態(tài)。eg: 一般實踐當中确沸,有很多應用級別的資源會被做成單例捌锭,比如配置文件信息,邏輯上來講罗捎,整個應用有且只能在同在時間有一個观谦,當然如果你有多個,這可能并不會引起程序級別錯誤桨菜,這里指的錯誤特指異郴碜矗或者ERROR。但是當我們試圖改變配置文件的時候倒得,問題就出來了泻红。
在我的工作過程中,我發(fā)現(xiàn)所有可以使用單例模式的類都有一個共性霞掺,那就是這個類沒有自己的狀態(tài)谊路,換句話說,這些類無論你實例化多少個菩彬,其實都是一樣的缠劝,而且更重要的一點是潮梯,這個類如果有兩個或者兩個以上的實例的話,我的程序竟然會產(chǎn)生程序錯誤或者與現(xiàn)實相違背的邏輯錯誤惨恭。
這樣的話秉馏,如果我們不將這個類控制成單例的結(jié)構(gòu),應用中就會存在很多一模一樣的類實例脱羡,這會非常浪費系統(tǒng)的內(nèi)存資源萝究,而且容易導致錯誤甚至一定會產(chǎn)生錯誤,所以我們單例模式所期待的目標或者說使用它的目的轻黑,是為了盡可能的節(jié)約內(nèi)存空間糊肤,減少無謂的GC消耗,并且使應用可以正常運作氓鄙。
寫法:
1. 最傻的寫法:
public class SingleInstance{
private static SingleInstance singleton;
private SingleInstance(){
//doInit
}
public static SingleInstance getInstance(){
if(singleton == null){
singleton = new SingleInstance();
}
}
}
這種就不多說了馆揉,在多個線程執(zhí)行下可能會創(chuàng)建出很多個實例不能保證單例。
2. 加上鎖的第一種寫法:
public class BadSynchronizedSingleton {
//一個靜態(tài)的實例
private static BadSynchronizedSingleton synchronizedSingleton;
//私有化構(gòu)造函數(shù)
private BadSynchronizedSingleton(){}
//給出一個公共的靜態(tài)方法返回一個單一實例
public synchronized static BadSynchronizedSingleton getInstance(){
if (synchronizedSingleton == null) {
synchronizedSingleton = new BadSynchronizedSingleton();
}
return synchronizedSingleton;
}
}
這種做法把整個方法鎖住確實是能解決上面說的問題抖拦,但是凡是一個線程使用getInstance就會把這個對象鎖住升酣。造成很多無謂的等待。
3. 單例模式版本态罪,也稱為雙重加鎖:
public class SynchronizedSingleton {
//一個靜態(tài)的實例
private static SynchronizedSingleton synchronizedSingleton;
//私有化構(gòu)造函數(shù)
private SynchronizedSingleton(){}
//給出一個公共的靜態(tài)方法返回一個單一實例
public static SynchronizedSingleton getInstance(){
if (synchronizedSingleton == null) {
synchronized (SynchronizedSingleton.class) {
if (synchronizedSingleton == null) {
synchronizedSingleton = new SynchronizedSingleton();
}
}
}
return synchronizedSingleton;
}
}
這種做法與上面那種最無腦的同步做法相比就要好很多了噩茄,因為我們只是在當前實例為null,也就是實例還未創(chuàng)建時才進行同步复颈,否則就直接返回绩聘,這樣就節(jié)省了很多無謂的線程等待時間,值得注意的是在同步塊中耗啦,我們再次判斷了synchronizedSingleton是否為null凿菩,解釋下為什么要這樣做。
假設(shè)我們?nèi)サ敉綁K中的是否為null的判斷帜讲,有這樣一種情況衅谷,假設(shè)A線程和B線程都在同步塊外面判斷了synchronizedSingleton為null,結(jié)果A線程首先獲得了線程鎖似将,進入了同步塊获黔,然后A線程會創(chuàng)造一個實例,此時synchronizedSingleton已經(jīng)被賦予了實例在验,A線程退出同步塊玷氏,直接返回了第一個創(chuàng)造的實例,此時B線程獲得線程鎖腋舌,也進入同步塊预茄,此時A線程其實已經(jīng)創(chuàng)造好了實例,B線程正常情況應該直接返回的,但是因為同步塊里沒有判斷是否為null耻陕,直接就是一條創(chuàng)建實例的語句拙徽,所以B線程也會創(chuàng)造一個實例返回,此時就造成創(chuàng)造了多個實例的情況诗宣。
經(jīng)過剛才的分析膘怕,貌似上述雙重加鎖的示例看起來是沒有問題了,但如果再進一步深入考慮的話召庞,其實仍然是有問題的岛心。
如果我們深入到JVM中去探索上面這段代碼,它就有可能(注意篮灼,只是有可能)是有問題的忘古。
因為虛擬機在執(zhí)行創(chuàng)建實例的這一步操作的時候,其實是分了好幾步去進行的诅诱,也就是說創(chuàng)建一個新的對象并非是原子性操作髓堪。在有些JVM中上述做法是沒有問題的,但是有些情況下是會造成莫名的錯誤娘荡。
首先要明白在JVM創(chuàng)建新的對象時干旁,主要要經(jīng)過三步。
分配內(nèi)存
初始化構(gòu)造器
將對象指向分配的內(nèi)存的地址
這種順序在上述雙重加鎖的方式是沒有問題的炮沐,因為這種情況下JVM是完成了整個對象的構(gòu)造才將內(nèi)存的地址交給了對象争群。但是如果2和3步驟是相反的(2和3可能是相反的是因為JVM會針對字節(jié)碼進行調(diào)優(yōu),而其中的一項調(diào)優(yōu)便是調(diào)整指令的執(zhí)行順序)大年,就會出現(xiàn)問題了换薄。
因為這時將會先將內(nèi)存地址賦給對象,針對上述的雙重加鎖翔试,就是說先將分配好的內(nèi)存地址指給synchronizedSingleton轻要,然后再進行初始化構(gòu)造器,這時候后面的線程去請求getInstance方法時遏餐,會認為synchronizedSingleton對象已經(jīng)實例化了伦腐,直接返回一個引用赢底。如果在初始化構(gòu)造器之前失都,這個線程使用了synchronizedSingleton,就會產(chǎn)生莫名的錯誤幸冻。
所以我們在語言級別無法完全避免錯誤的發(fā)生粹庞,我們只有將該任務交給JVM,所以有一種比較標準的單例模式洽损。如下所示庞溜。
package com.oneinstance;
public class InnerClassSingleton {
public static Singleton getInstance(){
return Singleton.singleton;
}
private static class Singleton{
protected static Singleton singleton = new Singleton();
}
}
咱們上面說的都是懶漢式加載的方式,還有一種餓漢式加載的方式:
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return singleton;
}
}
從始至終就沒有使用這個實例,造成內(nèi)存的浪費流码。
不過在有些時候又官,直接初始化單例的實例也無傷大雅,對項目幾乎沒什么影響漫试,比如我們在應用啟動時就需要加載的配置文件等六敬,就可以采取這種方式去保證單例。