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

單例模式是設(shè)計(jì)模式中相對(duì)是來(lái)說(shuō)最容易理解的設(shè)計(jì)模式,但其實(shí)大多數(shù)人只是知道單例模式的一些簡(jiǎn)單場(chǎng)景的使用和實(shí)現(xiàn)。比如餓漢單例、懶漢單例等。實(shí)際上,在很多場(chǎng)景下,這些實(shí)現(xiàn)方式有很多問(wèn)題。那么在不同場(chǎng)景下我們應(yīng)該如何選擇單例的實(shí)現(xiàn)呢。這次我們會(huì)由淺入深的學(xué)習(xí),徹底的來(lái)認(rèn)識(shí)一下單例模式。

一、單例模式的定義

確保某一個(gè)類只有一個(gè)實(shí)例,而且自行實(shí)例化并向整個(gè)系統(tǒng)提供這個(gè)實(shí)例。

二、UML類圖

它的類圖很簡(jiǎn)單,就是client使用這個(gè)單例類就行了。
需要注意的是單例類的構(gòu)造方法是私有的。

單例UML.png

三、使用場(chǎng)景

它的使用場(chǎng)景也很明確。
確保某個(gè)類只要一個(gè)對(duì)象的場(chǎng)景,從而避免產(chǎn)生多個(gè)對(duì)象消耗過(guò)多的資源。

從定義、UML類圖和使用場(chǎng)景都透露出兩個(gè)字--簡(jiǎn)單。

可它的背后真的只是那么簡(jiǎn)單嗎?哈哈,表明簡(jiǎn)單的東西背后往往是不簡(jiǎn)單的。

嗯哼,不信,那就先放一張圖來(lái)震下場(chǎng)子

單例模式.png

這就是我這篇文章要說(shuō)的核心內(nèi)容了。

四、不同場(chǎng)景下的實(shí)現(xiàn)

1.說(shuō)實(shí)現(xiàn)之前,我們先要看看實(shí)現(xiàn)單例模式的關(guān)鍵點(diǎn)

(1)構(gòu)造方法不對(duì)外開(kāi)放。這個(gè)很容易理解,如果開(kāi)放了那么和話,外面隨便new一下就實(shí)現(xiàn)了一個(gè)對(duì)象,還單例個(gè)毛線呢。
(2)確保單例類的對(duì)象只有一個(gè),尤其是在多線程環(huán)境下。這個(gè)后面會(huì)詳細(xì)介紹。
(3)確保單例類的對(duì)象在反序列化時(shí)不會(huì)重新構(gòu)建對(duì)象。

那么下面我一一介紹各種實(shí)現(xiàn)方法和它們的優(yōu)缺點(diǎn)

2.最簡(jiǎn)單的實(shí)現(xiàn)方法--餓漢單例

餓漢,顧名思義,非常饑餓的漢子,所以一定義這個(gè)對(duì)象就給實(shí)例化了。

public class HungerSingleton {

    private static HungerSingleton singleton=new HungerSingleton();
    private HungerSingleton() {}
    
    public static HungerSingleton getSingleton() {
        return singleton;
    }
}

實(shí)現(xiàn)非常簡(jiǎn)單,通過(guò)一個(gè)靜態(tài)方法返回一個(gè)靜態(tài)對(duì)象。

但是缺點(diǎn)也很明顯,如果我不需要這個(gè)對(duì)象的話,它也會(huì)創(chuàng)建對(duì)象,浪費(fèi)空間資源和影響時(shí)間性能。

3.給饑餓的漢子一張床--懶漢單例

給了饑餓的漢子一張床后會(huì)忘記饑餓,但它會(huì)變懶。也就不急著實(shí)例化對(duì)象了,就是所謂的懶加載。

public class LazySingleton {

    private LazySingleton(){};
    
    private static LazySingleton singleton=null;
    
    public static  synchronized LazySingleton getSingleton() {
        if(singleton==null){
            singleton=new LazySingleton();
        }
        return singleton;
    }
}

仔細(xì)一看我們發(fā)現(xiàn)這個(gè)getSingleton方法加了個(gè)synchronize關(guān)鍵字。這個(gè)就是我們之前說(shuō)的”保證在多線程下對(duì)象唯一“的方法。

咋一看很完美,但還是有一個(gè)很大的問(wèn)題就是:

我們知道synchronize同步是比較消耗性能的,而且每次調(diào)用getSingleton方法都會(huì)進(jìn)行同步,這回造成不必要的性能開(kāi)銷(xiāo)。

4.懶漢單例的優(yōu)化--DCL單例

針對(duì)上面的懶漢單例的缺點(diǎn),出現(xiàn)了DCL(Double Check Lock)模式。其實(shí)解決的方式很簡(jiǎn)單了,只需要在外層加一個(gè)判空就OK了。

這樣就整個(gè)程序就有了兩次判空,第一次的目的是為了避免多余的同步操作。第二次是為了對(duì)象實(shí)例化的唯一性。

public class DCLSingleton {

    private DCLSingleton(){};

    private static DCLSingleton singleton=null;
    
    private static DCLSingleton getSingleton(){
        if (singleton==null) {
            synchronized (DCLSingleton.class) {
                if(singleton==null)
                    singleton=new DCLSingleton();
            }
        }
        return singleton;
    }
}

這里我們考慮到了多線程的情景又考慮到性能的問(wèn)題。看起來(lái)就完美了。其實(shí)不深入研究下去,這的確是完美的單例寫(xiě)法。那么還有什么不完美的地方呢?

就是這句話

singleton=new DCLSingleton();

這個(gè)語(yǔ)句他不是一個(gè)原子操作。

也就是說(shuō)

它表明上只有一步其實(shí)有三步操作

(1)給DCLSingleton的實(shí)例分配內(nèi)存

(2)調(diào)用Singleton()的構(gòu)造方法,初始化成員字段

(3)將singleton對(duì)象指向分配的內(nèi)存空間

由于java編譯器允許處理器亂序執(zhí)行,以及jdk1.5之前的java內(nèi)存模型(JMM)中寄存器、cache到主內(nèi)存回寫(xiě)順序的規(guī)定。所以不能保證上面三步執(zhí)行的順序。很有可能會(huì)按照(1)-(3)-(2)的順序
這就會(huì)有一個(gè)問(wèn)題,高并發(fā)的情況下,當(dāng)A線程執(zhí)行到(3),被切換到了B線程,那么B線程就會(huì)直接取走了singleton對(duì)象,但是該對(duì)象因?yàn)闆](méi)有執(zhí)行第二步,就沒(méi)有真正的實(shí)例化。所以就會(huì)被B拿去使用導(dǎo)致出錯(cuò),發(fā)生DCL失效。

那腫么辦呀?這個(gè)我們沒(méi)法控制啊。

幸運(yùn)的是,在jdk1.5的時(shí)候,出現(xiàn)了一個(gè)volatile關(guān)鍵字。當(dāng)我們的jdk版本大于等于1.5的時(shí)候,只要在定義的時(shí)候加上這個(gè)volatile就能夠解決了問(wèn)題了。

private static volatile DCLSingleton singleton=null;

雖然有點(diǎn)小瑕疵但是DCL的優(yōu)點(diǎn)還是多的,大多數(shù)情景下可以保證單例對(duì)象的唯一性。但如果你的并發(fā)場(chǎng)景復(fù)雜或者版本低于jdk1.5的時(shí)候,可能需要一個(gè)更好的實(shí)現(xiàn)方式了。

5.《java并發(fā)編程實(shí)踐》推薦--靜態(tài)內(nèi)部類單例

java并發(fā)編程實(shí)踐書(shū)中并不贊同DCL實(shí)現(xiàn)方式,因?yàn)檫@種方式還是會(huì)出現(xiàn)DCL失效。它建議使用靜態(tài)內(nèi)部類實(shí)現(xiàn)

public class InternalClassSingleton {
    private InternalClassSingleton(){};
    
    public static InternalClassSingleton getSingleton(){
        return SingletonHolder.singleton;
    }
    
    private static class SingletonHolder{
        private static final InternalClassSingleton singleton=new InternalClassSingleton();
    }

}

這種方式也是很簡(jiǎn)潔的,它只會(huì)在第一次調(diào)用getSingleton()的時(shí)候才會(huì)讓singleton被初始化。因此第一次加載getSingleton的時(shí)候會(huì)導(dǎo)致虛擬機(jī)加載SingletonHolder類,這種方式不僅能夠保證單例對(duì)象的唯一性,同時(shí)也延遲了單例的實(shí)例化。推薦使用的單例模式實(shí)現(xiàn)方式。

6.最簡(jiǎn)單的單例實(shí)現(xiàn)--枚舉單例

我們都知道枚舉和普通的java類是一樣的,不僅有字段還有自己的方法。重要的是默認(rèn)枚舉實(shí)例的創(chuàng)建是線程安全的,并且在任何情況下它只有一個(gè)單例。

public class EnumSingleton {
    private EnumSingleton(){};
    enum Enum{
        INSTANCE;
        public static Enum getInstance(){
            return INSTANCE;
        }
    }
}

實(shí)在是簡(jiǎn)單,看著還舒服,重點(diǎn)不需要考慮多線程的場(chǎng)景問(wèn)題。美滋滋~~

除了上面說(shuō)到,其實(shí)還有其他的方式實(shí)現(xiàn),比如利用容器等等。但上面的方式足以應(yīng)對(duì)各種的場(chǎng)景了,我們只需要根據(jù)不同的場(chǎng)景和自己的喜好來(lái)選擇合適的就好了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容