單例模式

轉(zhuǎn)自單例模式詳解 稍作修改

世間萬(wàn)物都有它的起源,那單利模式的誕生原因或者說情景是怎么樣的呢?
單例模式的類都有一個(gè)共性,那就是這個(gè)類沒有自己的狀態(tài),換句話說,這些類無(wú)論你實(shí)例化多少個(gè),其實(shí)都是一樣的,而且更重要的一點(diǎn)是,這個(gè)類如果有兩個(gè)或者兩個(gè)以上的實(shí)例的話,程序竟然會(huì)產(chǎn)生程序錯(cuò)誤或者與現(xiàn)實(shí)相違背的邏輯錯(cuò)誤。
這樣的話,如果我們不將這個(gè)類控制成單例的結(jié)構(gòu),應(yīng)用中就會(huì)存在很多一模一樣的類實(shí)例,這會(huì)非常浪費(fèi)系統(tǒng)的內(nèi)存資源,而且容易導(dǎo)致錯(cuò)誤甚至一定會(huì)產(chǎn)生錯(cuò)誤,所以我們單例模式所期待的目標(biāo)或者說使用它的目的,是為了盡可能的節(jié)約內(nèi)存空間,減少無(wú)謂的GC消耗,并且使應(yīng)用可以正常運(yùn)作。

我稍微總結(jié)一下,一般一個(gè)類能否做成單例,最容易區(qū)別的地方就在于,這些類,在應(yīng)用中如果有兩個(gè)或者兩個(gè)以上的實(shí)例會(huì)引起錯(cuò)誤,又或者我換句話說,就是這些類,在整個(gè)應(yīng)用中,同一時(shí)刻,有且只能有一種狀態(tài)。

一般實(shí)踐當(dāng)中,有很多應(yīng)用級(jí)別的資源會(huì)被做成單例,比如配置文件信息,邏輯上來講,整個(gè)應(yīng)用有且只能在同在時(shí)間有一個(gè),當(dāng)然如果你有多個(gè),這可能并不會(huì)引起程序級(jí)別錯(cuò)誤,這里指的錯(cuò)誤特指異常或者ERROR。但是當(dāng)我們?cè)噲D改變配置文件的時(shí)候,問題就出來了。
你有兩種選擇,第一種,將所有的實(shí)例全部更新成一模一樣的狀態(tài)。第二種,就是等著出現(xiàn)問題。

然而出現(xiàn)的問題大部分是邏輯層次上的錯(cuò)誤,個(gè)人覺得這是比程序錯(cuò)誤更加嚴(yán)重的錯(cuò)誤,因?yàn)樗粫?huì)告訴你空指針,不會(huì)告訴你非法參數(shù),很多時(shí)候要等到影響到客戶使用時(shí)才會(huì)被發(fā)現(xiàn)。
下面,我們就來看一下做成單例的幾種方式。
第一種方式,我們來看一下最標(biāo)準(zhǔn)也是最原始的單例模式的構(gòu)造方式。

public class Singleton {

    //一個(gè)靜態(tài)的實(shí)例
    private static Singleton singleton;
    //私有化構(gòu)造函數(shù)
    private Singleton(){}
    //給出一個(gè)公共的靜態(tài)方法返回一個(gè)單一實(shí)例
    public static Singleton getInstance(){
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

這是在不考慮并發(fā)訪問的情況下標(biāo)準(zhǔn)的單例模式的構(gòu)造方式,這種方式通過幾個(gè)地方來限制了我們?nèi)〉降膶?shí)例是唯一的。
1.靜態(tài)實(shí)例,帶有static關(guān)鍵字的屬性在每一個(gè)類中都是唯一的。
** 2.限制客戶端隨意創(chuàng)造實(shí)例,即私有化構(gòu)造方法,此為保證單例的最重要的一步。**
** 3.給一個(gè)公共的獲取實(shí)例的靜態(tài)方法,注意,是靜態(tài)的方法,因?yàn)檫@個(gè)方法是在我們未獲取到實(shí)例的時(shí)候就要提供給客戶端調(diào)用的,所以如果是非靜態(tài)的話,那就變成一個(gè)矛盾體了,因?yàn)榉庆o態(tài)的方法必須要擁有實(shí)例才可以調(diào)用。**
** 4.判斷只有持有的靜態(tài)實(shí)例為null時(shí)才調(diào)用構(gòu)造方法創(chuàng)造一個(gè)實(shí)例,否則就直接返回。**

假如你去面試一家公司,給了你一道題,讓你寫出一個(gè)單例模式的例子,那么如果你是剛出大學(xué)校門的學(xué)生,你能寫出上面這種示例,假設(shè)我是面試官的話,滿分100的話,我會(huì)給90分,剩下的那10分算是給更優(yōu)秀的人一個(gè)更高的臺(tái)階。但如果你是一個(gè)有過兩三年工作經(jīng)驗(yàn)的人,如果你寫出上面的示例,我估計(jì)我最多給你30分,甚至心情要是萬(wàn)一不好的話可能會(huì)一分不給。
為什么同樣的示例放到不同的人身上差別會(huì)這么大,就是因?yàn)榍懊嫖姨岬降哪莻€(gè)情況,在不考慮并發(fā)訪問的情況下,上述示例是沒有問題的。
至于為什么在并發(fā)情況下上述的例子是不安全的呢,我在這里給各位制造了一個(gè)并發(fā)的例子,用來說明,上述情況的單例模式,是有可能造出來多個(gè)實(shí)例的,我自己測(cè)試了約莫100次左右,最多的一次,竟然造出了3個(gè)實(shí)例。下面給出代碼,大約運(yùn)行10次(并發(fā)是具有概率性的,10次只是保守估計(jì),也可能一次,也可能100次)就會(huì)發(fā)現(xiàn)我們創(chuàng)造了不只一個(gè)實(shí)例。

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestSingleton {
    
    boolean lock ;
    
    public boolean isLock() {
        return lock;
    }

    public void setLock(boolean lock) {
        this.lock = lock;
    }
    
    public static void main(String[] args) throws InterruptedException {
        final Set<String> instanceSet = Collections.synchronizedSet(new HashSet<String>());
        final TestSingleton lock = new TestSingleton();
        lock.setLock(true);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 100; i++) {
            executorService.execute(new Runnable() {
                
                public void run() {
                    while (true) {
                        if (!lock.isLock()) {
                            Singleton singleton = Singleton.getInstance();
                            instanceSet.add(singleton.toString());
                            break;
                        }
                    }
                }
            });
        }
        Thread.sleep(5000);
        lock.setLock(false);
        Thread.sleep(5000);
        System.out.println("------并發(fā)情況下我們?nèi)〉降膶?shí)例------");
        for (String instance : instanceSet) {
            System.out.println(instance);
        }
        executorService.shutdown();
    }
}

我在程序中同時(shí)開啟了100個(gè)線程,去訪問getInstance方法,并且把獲得實(shí)例的toString方法獲得的實(shí)例字符串裝入一個(gè)同步的set集合,set集合會(huì)自動(dòng)去重,所以看結(jié)果如果輸出了兩個(gè)或者兩個(gè)以上的實(shí)例字符串,就說明我們?cè)诓l(fā)訪問的過程中產(chǎn)生了多個(gè)實(shí)例。
程序當(dāng)中讓main線程睡眠了兩次,第一次是為了給足夠的時(shí)間讓100個(gè)線程全部開啟,第二個(gè)是將鎖打開以后,保證所有的線程都已經(jīng)調(diào)用了getInstance方法。
好了,這下我們用事實(shí)說明了,上述的單例寫法,我們是可以創(chuàng)造出多個(gè)實(shí)例的,至于為什么在這里要稍微解釋一下,雖說我一直都喜歡用事實(shí)說話,包括看書的時(shí)候,我也不喜歡作者跟我解釋為什么,而是希望給我一個(gè)例子,讓我自己去印證。
造成這種情況的原因是因?yàn)?,?dāng)并發(fā)訪問的時(shí)候,第一個(gè)調(diào)用getInstance方法的線程A,在判斷完singleton是null的時(shí)候,線程A就進(jìn)入了if塊準(zhǔn)備創(chuàng)造實(shí)例,但是同時(shí)另外一個(gè)線程B在線程A還未創(chuàng)造出實(shí)例之前,就又進(jìn)行了singleton是否為null的判斷,這時(shí)singleton依然為null,所以線程B也會(huì)進(jìn)入if塊去創(chuàng)造實(shí)例,這時(shí)問題就出來了,有兩個(gè)線程都進(jìn)入了if塊去創(chuàng)造實(shí)例,結(jié)果就造成單例模式并非單例。
為了避免這種情況,我們就要考慮并發(fā)的情況了,我們最容易想到的方式應(yīng)該是下面這樣的方式,直接將整個(gè)方法同步。

public class BadSynchronizedSingleton {

    //一個(gè)靜態(tài)的實(shí)例
    private static BadSynchronizedSingleton synchronizedSingleton;
    //私有化構(gòu)造函數(shù)
    private BadSynchronizedSingleton(){}
    //給出一個(gè)公共的靜態(tài)方法返回一個(gè)單一實(shí)例
    public synchronized static BadSynchronizedSingleton getInstance(){
        if (synchronizedSingleton == null) {
            synchronizedSingleton = new BadSynchronizedSingleton();
        }
        return synchronizedSingleton;
    }
    
}

上面的做法很簡(jiǎn)單,就是將整個(gè)獲取實(shí)例的方法同步,這樣在一個(gè)線程訪問這個(gè)方法時(shí),其它所有的線程都要處于掛起等待狀態(tài),倒是避免了剛才同步訪問創(chuàng)造出多個(gè)實(shí)例的危險(xiǎn),但是我只想說,這樣的設(shè)計(jì)實(shí)在是糟糕透了,這樣會(huì)造成很多無(wú)謂的等待,所以為了表示我的憤怒,我在類名上加入Bad。

其實(shí)我們同步的地方只是需要發(fā)生在單例的實(shí)例還未創(chuàng)建的時(shí)候,在實(shí)例創(chuàng)建以后,獲取實(shí)例的方法就沒必要再進(jìn)行同步控制了,所以我們將上面的示例改為很多教科書中標(biāo)準(zhǔn)的單例模式版本,也稱為雙重加鎖

public class SynchronizedSingleton {

    //一個(gè)靜態(tài)的實(shí)例
    private static SynchronizedSingleton synchronizedSingleton;
    //私有化構(gòu)造函數(shù)
    private SynchronizedSingleton(){}
    //給出一個(gè)公共的靜態(tài)方法返回一個(gè)單一實(shí)例
    public static SynchronizedSingleton getInstance(){
        if (synchronizedSingleton == null) {
            synchronized (SynchronizedSingleton.class) {
                if (synchronizedSingleton == null) {
                    synchronizedSingleton = new SynchronizedSingleton();
                }
            }
        }
        return synchronizedSingleton;
    }
}

這種做法與上面那種最無(wú)腦的同步做法相比就要好很多了,因?yàn)槲覀冎皇窃诋?dāng)前實(shí)例為null,也就是實(shí)例還未創(chuàng)建時(shí)才進(jìn)行同步,否則就直接返回,這樣就節(jié)省了很多無(wú)謂的線程等待時(shí)間,值得注意的是在同步塊中,我們?cè)俅闻袛嗔藄ynchronizedSingleton是否為null,解釋下為什么要這樣做。

假設(shè)我們?nèi)サ敉綁K中的是否為null的判斷,有這樣一種情況,假設(shè)A線程和B線程都在同步塊外面判斷了synchronizedSingleton為null,結(jié)果A線程首先獲得了線程鎖,進(jìn)入了同步塊,然后A線程會(huì)創(chuàng)造一個(gè)實(shí)例,此時(shí)synchronizedSingleton已經(jīng)被賦予了實(shí)例,A線程退出同步塊,直接返回了第一個(gè)創(chuàng)造的實(shí)例,此時(shí)B線程獲得線程鎖,也進(jìn)入同步塊,此時(shí)A線程其實(shí)已經(jīng)創(chuàng)造好了實(shí)例,B線程正常情況應(yīng)該直接返回的,但是因?yàn)橥綁K里沒有判斷是否為null,直接就是一條創(chuàng)建實(shí)例的語(yǔ)句,所以B線程也會(huì)創(chuàng)造一個(gè)實(shí)例返回,此時(shí)就造成創(chuàng)造了多個(gè)實(shí)例的情況。

經(jīng)過剛才的分析,貌似上述雙重加鎖的示例看起來是沒有問題了,但如果再進(jìn)一步深入考慮的話,其實(shí)仍然是有問題的。

如果我們深入到JVM中去探索上面這段代碼,它就有可能(注意,只是有可能)是有問題的。

因?yàn)樘摂M機(jī)在執(zhí)行創(chuàng)建實(shí)例的這一步操作的時(shí)候,其實(shí)是分了好幾步去進(jìn)行的,也就是說創(chuàng)建一個(gè)新的對(duì)象并非是原子性操作。在有些JVM中上述做法是沒有問題的,但是有些情況下是會(huì)造成莫名的錯(cuò)誤。

首先要明白在JVM創(chuàng)建新的對(duì)象時(shí),主要要經(jīng)過三步。
** 1.分配內(nèi)存**
** 2.初始化構(gòu)造器**
** 3.將對(duì)象指向分配的內(nèi)存的地址**
這種順序在上述雙重加鎖的方式是沒有問題的,因?yàn)檫@種情況下JVM是完成了整個(gè)對(duì)象的構(gòu)造才將內(nèi)存的地址交給了對(duì)象。但是如果2和3步驟是相反的(2和3可能是相反的是因?yàn)镴VM會(huì)針對(duì)字節(jié)碼進(jìn)行調(diào)優(yōu),而其中的一項(xiàng)調(diào)優(yōu)便是調(diào)整指令的執(zhí)行順序),就會(huì)出現(xiàn)問題了。
因?yàn)檫@時(shí)將會(huì)先將內(nèi)存地址賦給對(duì)象,針對(duì)上述的雙重加鎖,就是說先將分配好的內(nèi)存地址指給synchronizedSingleton,然后再進(jìn)行初始化構(gòu)造器,這時(shí)候后面的線程去請(qǐng)求getInstance方法時(shí),會(huì)認(rèn)為synchronizedSingleton對(duì)象已經(jīng)實(shí)例化了,直接返回一個(gè)引用。如果在初始化構(gòu)造器之前,這個(gè)線程使用了synchronizedSingleton,就會(huì)產(chǎn)生莫名的錯(cuò)誤。
所以我們?cè)谡Z(yǔ)言級(jí)別無(wú)法完全避免錯(cuò)誤的發(fā)生,我們只有將該任務(wù)交給JVM,所以有一種比較標(biāo)準(zhǔn)的單例模式。如下所示。

package com.oneinstance;

public class InnerClassSingleton {
    
    public static Singleton getInstance(){
        return Singleton.singleton;
    }

    private static class Singleton{
        
        protected static Singleton singleton = new Singleton();
        
    }
}

首先來說一下,這種方式為何會(huì)避免了上面莫名的錯(cuò)誤,主要是因?yàn)橐粋€(gè)類的靜態(tài)屬性只會(huì)在第一次加載類時(shí)初始化,這是JVM幫我們保證的,所以我們無(wú)需擔(dān)心并發(fā)訪問的問題。所以在初始化進(jìn)行一半的時(shí)候,別的線程是無(wú)法使用的,因?yàn)镴VM會(huì)幫我們強(qiáng)行同步這個(gè)過程。另外由于靜態(tài)變量只初始化一次,所以singleton仍然是單例的。

上面這種寫法是我們使用靜態(tài)的內(nèi)部類作為單例,這樣不太符合我們的習(xí)慣。所以我們改為以下形式。

public class Singleton {
    
    private Singleton(){}
    
    public static Singleton getInstance(){
        return SingletonInstance.instance;
    }
    
    private static class SingletonInstance{
        
        static Singleton instance = new Singleton();
        
    }
}

ps:加載一個(gè)類時(shí),其內(nèi)部類不會(huì)同時(shí)被加載。一個(gè)類被加載,當(dāng)且僅當(dāng)其某個(gè)靜態(tài)成員(靜態(tài)域、構(gòu)造器、靜態(tài)方法等)被調(diào)用時(shí)發(fā)生。 所以以上做法與通常的餓漢型加載方式有所不同,雖然同樣利用了classloder的機(jī)制來保證初始化instance時(shí)只有一個(gè)線程,但只有當(dāng)用戶調(diào)用getInstance()方法時(shí),才進(jìn)行instance 加載,是一種懶加載的體現(xiàn)

好了,進(jìn)行到這里,單例模式算是已經(jīng)完成了。最終的產(chǎn)物就是如上述的形式。上述形式保證了以下幾點(diǎn)。

1.Singleton最多只有一個(gè)實(shí)例,在不考慮反射強(qiáng)行突破訪問限制的情況下。
2.保證了并發(fā)訪問的情況下,不會(huì)發(fā)生由于并發(fā)而產(chǎn)生多個(gè)實(shí)例。
3.保證了并發(fā)訪問的情況下,不會(huì)由于初始化動(dòng)作未完全完成而造成使用了尚未正確初始化的實(shí)例。
以下為不太常用的方式,這里給出來只是給各位參考,不建議使用下述方式。

第一種,就是俗稱的餓漢式加載

public class Singleton {
    
    private static Singleton singleton = new Singleton();
    
    private Singleton(){}
    
    public static Singleton getInstance(){
        return singleton;
    }
    
}

上述方式與我們最后一種給出的方式類似,只不過沒有經(jīng)過內(nèi)部類處理,這種方式最主要的缺點(diǎn)就是一旦我訪問了Singleton的任何其他的靜態(tài)域,就會(huì)造成實(shí)例的初始化,而事實(shí)是可能我們從始至終就沒有使用這個(gè)實(shí)例,造成內(nèi)存的浪費(fèi)。

不過在有些時(shí)候,直接初始化單例的實(shí)例也無(wú)傷大雅,對(duì)項(xiàng)目幾乎沒什么影響,比如我們?cè)趹?yīng)用啟動(dòng)時(shí)就需要加載的配置文件等,就可以采取這種方式去保證單例。

第二種我就不貼了,與雙重鎖定一模一樣,只是給靜態(tài)的實(shí)例屬性加上關(guān)鍵字volatile,標(biāo)識(shí)這個(gè)屬性是不需要優(yōu)化的。

這樣也不會(huì)出現(xiàn)實(shí)例化發(fā)生一半的情況,因?yàn)榧尤肓藇olatile關(guān)鍵字,就等于禁止了JVM自動(dòng)的指令重排序優(yōu)化,并且強(qiáng)行保證線程中對(duì)變量所做的任何寫入操作對(duì)其他線程都是即時(shí)可見的。這里沒有篇幅去介紹volatile以及JVM中變量訪問時(shí)所做的具體動(dòng)作,總之volatile會(huì)強(qiáng)行將對(duì)該變量的所有讀和取操作綁定成一個(gè)不可拆分的動(dòng)作。如果讀者有興趣的話,可以自行去找一些資料看一下相關(guān)內(nèi)容。

不過值得注意的是,volatile關(guān)鍵字是在JDK1.5以及1.5之后才被給予了意義,所以這種方式要在JDK1.5以及1.5之后才可以使用,但仍然還是不推薦這種方式,一是因?yàn)榇a相對(duì)復(fù)雜,二是因?yàn)橛捎贘DK版本的限制有時(shí)候會(huì)有諸多不便。

好了,以上基本上就是常見的所有單例模式的構(gòu)造方式,如果下次再有面試讓你去寫一個(gè)單例模式,有時(shí)間的話就把上面所有的全部寫給面試官并一一將優(yōu)劣講給他聽吧,這樣的話估計(jì)offer已經(jīng)離你不遠(yuǎn)了。

本次單例模式的分享就到此結(jié)束了,感謝各位的收看。

最后編輯于
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 單例模式(SingletonPattern)一般被認(rèn)為是最簡(jiǎn)單、最易理解的設(shè)計(jì)模式,也因?yàn)樗暮?jiǎn)潔易懂,是項(xiàng)目中最...
    成熱了閱讀 4,530評(píng)論 4 34
  • 1 場(chǎng)景問題# 1.1 讀取配置文件的內(nèi)容## 考慮這樣一個(gè)應(yīng)用,讀取配置文件的內(nèi)容。 很多應(yīng)用項(xiàng)目,都有與應(yīng)用相...
    七寸知架構(gòu)閱讀 6,966評(píng)論 12 68
  • 前言 本文主要參考 那些年,我們一起寫過的“單例模式”。 何為單例模式? 顧名思義,單例模式就是保證一個(gè)類僅有一個(gè)...
    tandeneck閱讀 2,623評(píng)論 1 8
  • 1.單例模式概述 (1)引言 單例模式是應(yīng)用最廣的模式之一,也是23種設(shè)計(jì)模式中最基本的一個(gè)。本文旨在總結(jié)通過Ja...
    曹豐斌閱讀 3,062評(píng)論 6 47
  • 還未到那荒涼的夜晚 卻已想那片暖 在那叢中 呢喃著的鳥兒 和你目光 倒映著焰火 是我 燃燒如今晚 忘卻了 ...
    五更雪閱讀 224評(píng)論 0 0

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