設(shè)計(jì)模式-單例模式(Singleton)

單例模式(Singleton)

單例對(duì)象(Singleton)是一種常用的設(shè)計(jì)模式。在Java應(yīng)用中,單例對(duì)象能保證在一個(gè)JVM中,該對(duì)象只有一個(gè)實(shí)例存在。

這樣的模式有幾個(gè)好處:

  1. 某些類(lèi)創(chuàng)建比較頻繁,對(duì)于一些大型的對(duì)象,這是一筆很大的系統(tǒng)開(kāi)銷(xiāo)。

  2. 省去了new操作符,降低了系統(tǒng)內(nèi)存的使用頻率,減輕GC壓力。

  3. 有些類(lèi)如交易所的核心交易引擎,控制著交易流程,如果該類(lèi)可以創(chuàng)建多個(gè)的話(huà),系統(tǒng)完全亂了。(比如一個(gè)軍隊(duì)出現(xiàn)了多個(gè)司令員同時(shí)指揮,肯定會(huì)亂成一團(tuán)),所以只有使用單例模式,才能保證核心交易服務(wù)器獨(dú)立控制整個(gè)流程。

首先我們寫(xiě)一個(gè)簡(jiǎn)單的單例類(lèi):

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è)類(lèi)可以滿(mǎn)足基本要求,但是,像這樣毫無(wú)線(xiàn)程安全保護(hù)的類(lèi),如果我們把它放入多線(xiàn)程的環(huán)境下,肯定就會(huì)出現(xiàn)問(wè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;  
}  

似乎解決了之前提到的問(wèn)題,將synchronized關(guān)鍵字加在了內(nèi)部,也就是說(shuō)當(dāng)調(diào)用的時(shí)候是不需要加鎖的,只有在instance為null,并創(chuàng)建對(duì)象的時(shí)候才需要加鎖,性能有一定的提升。

但是,這樣的情況,還是有可能有問(wèn)題的,看下面的情況:

在Java指令中創(chuàng)建對(duì)象和賦值操作是分開(kāi)進(jìn)行的,也就是說(shuō)instance = new Singleton();語(yǔ)句是分兩步執(zhí)行的。但是JVM并不保證這兩個(gè)操作的先后順序,也就是說(shuō)有可能JVM會(huì)為新的Singleton實(shí)例分配空間,然后直接賦值給instance成員,然后再去初始化這個(gè)Singleton實(shí)例。這樣就可能出錯(cuò)了,我們以A、B兩個(gè)線(xiàn)程為例:

  1. A、B線(xiàn)程同時(shí)進(jìn)入了第一個(gè)if判斷

  2. A首先進(jìn)入synchronized塊,由于instance為null,所以它執(zhí)行instance = new Singleton();

  3. 由于JVM內(nèi)部的優(yōu)化機(jī)制,JVM先畫(huà)出了一些分配給Singleton實(shí)例的空白內(nèi)存,并賦值給instance成員(注意此時(shí)JVM沒(méi)有開(kāi)始初始化這個(gè)實(shí)例),然后A離開(kāi)了synchronized塊。

  4. B進(jìn)入synchronized塊,由于instance此時(shí)不是null,因此它馬上離開(kāi)了synchronized塊并將結(jié)果返回給調(diào)用該方法的程序。

  5. 此時(shí)B線(xiàn)程打算使用Singleton實(shí)例,卻發(fā)現(xiàn)它沒(méi)有被初始化,于是錯(cuò)誤發(fā)生了。

所以程序還是有可能發(fā)生錯(cuò)誤,其實(shí)程序在運(yùn)行過(guò)程是很復(fù)雜的,從這點(diǎn)我們就可以看出,尤其是在寫(xiě)多線(xià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)部類(lèi)來(lái)維護(hù)單例的實(shí)現(xiàn),JVM內(nèi)部的機(jī)制能夠保證當(dāng)一個(gè)類(lèi)被加載的時(shí)候,這個(gè)類(lèi)的加載過(guò)程是線(xiàn)程互斥的。這樣當(dāng)我們第一次調(diào)用getInstance的時(shí)候,JVM能夠幫我們保證instance只被創(chuàng)建一次,并且會(huì)保證把賦值給instance的內(nèi)存初始化完畢,這樣我們就不用擔(dān)心上面的問(wèn)題。同時(shí)該方法也只會(huì)在第一次調(diào)用的時(shí)候使用互斥機(jī)制,這樣就解決了低性能問(wèn)題。

這樣我們暫時(shí)總結(jié)一個(gè)完美的單例模式:

public class Singleton {  
  
    /* 私有構(gòu)造方法,防止被實(shí)例化 */  
    private Singleton() {  
    }  
  
    /* 此處使用一個(gè)內(nèi)部類(lèi)來(lá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í)說(shuō)它完美,也不一定,如果在構(gòu)造函數(shù)中拋出異常,實(shí)例將永遠(yuǎn)得不到創(chuàng)建,也會(huì)出錯(cuò)。所以說(shuō),十分完美的東西是沒(méi)有的,我們只能根據(jù)實(shí)際情況,選擇最適合自己應(yīng)用場(chǎng)景的實(shí)現(xiàn)方法。也有人這樣實(shí)現(xiàn):因?yàn)槲覀冎恍枰趧?chuàng)建類(lèi)的時(shí)候進(jìn)行同步,所以只要將創(chuàng)建和getInstance()分開(kāi),單獨(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;  
    }  
}  

考慮性能的話(huà),整個(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();  
    }  
}  

通過(guò)單例模式的學(xué)習(xí)告訴我們:

  1. 單例模式理解起來(lái)簡(jiǎn)單,但是具體實(shí)現(xiàn)起來(lái)還是有一定的難度。

  2. synchronized關(guān)鍵字鎖定的是對(duì)象,在用的時(shí)候,一定要在恰當(dāng)?shù)牡胤绞褂茫ㄗ⒁庑枰褂面i的對(duì)象和過(guò)程,可能有的時(shí)候并不是整個(gè)對(duì)象及整個(gè)過(guò)程都需要鎖)。

到這兒,單例模式基本已經(jīng)講完了,結(jié)尾處,筆者突然想到另一個(gè)問(wèn)題,就是采用類(lèi)的靜態(tài)方法,實(shí)現(xiàn)單例模式的效果,也是可行的,此處二者有什么不同?

首先,靜態(tài)類(lèi)不能實(shí)現(xiàn)接口。(從類(lèi)的角度說(shuō)是可以的,但是那樣就破壞了靜態(tài)了。因?yàn)榻涌谥胁辉试S有static修飾的方法,所以即使實(shí)現(xiàn)了也是非靜態(tài)的)

其次,單例可以被延遲初始化,靜態(tài)類(lèi)一般在第一次加載是初始化。之所以延遲加載,是因?yàn)橛行╊?lèi)比較龐大,所以延遲加載有助于提升性能。

再次,單例類(lèi)可以被繼承,他的方法可以被覆寫(xiě)。但是靜態(tài)類(lèi)內(nèi)部方法都是static,無(wú)法被覆寫(xiě)。

最后一點(diǎn),單例類(lèi)比較靈活,畢竟從實(shí)現(xiàn)上只是一個(gè)普通的Java類(lèi),只要滿(mǎn)足單例的基本需求,你可以在里面隨心所欲的實(shí)現(xiàn)一些其它功能,但是靜態(tài)類(lèi)不行。

從上面這些概括中,基本可以看出二者的區(qū)別,但是,從另一方面講,我們上面最后實(shí)現(xiàn)的那個(gè)單例模式,內(nèi)部就是用一個(gè)靜態(tài)類(lèi)來(lái)實(shí)現(xiàn)的,所以,二者有很大的關(guān)聯(lián),只是我們考慮問(wèn)題的層面不同罷了。兩種思想的結(jié)合,才能造就出完美的解決方案,就像HashMap采用數(shù)組+鏈表來(lái)實(shí)現(xiàn)一樣,其實(shí)生活中很多事情都是這樣,單用不同的方法來(lái)處理問(wèn)題,總是有優(yōu)點(diǎn)也有缺點(diǎn),最完美的方法是,結(jié)合各個(gè)方法的優(yōu)點(diǎn),才能最好的解決問(wèn)題!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容