單例模式各版本的原理與實(shí)踐

1.單例模式概述

(1)引言

單例模式是應(yīng)用最廣的模式之一,也是23種設(shè)計(jì)模式中最基本的一個(gè)。本文旨在總結(jié)通過(guò)Java實(shí)現(xiàn)單例模式的各個(gè)版本的優(yōu)缺點(diǎn)及適用場(chǎng)景,詳細(xì)分析如何實(shí)現(xiàn)線程安全的單例模式,并探討單例模式的一些擴(kuò)展。

(2)單例模式的定義

??Ensure a class has only one instance,and provide a global point of access to it.(確保某一個(gè)類只有一個(gè)實(shí)例,并且自行實(shí)例化并向整個(gè)系統(tǒng)提供這個(gè)實(shí)例)
通用類圖為:

Singleton類稱為單例類,通過(guò)使用private的構(gòu)造函數(shù)確保了在一個(gè)應(yīng)用中只產(chǎn)生一個(gè)實(shí)例,并且是自行實(shí)例化的(在Singleton中自己使用new Singleton())。

(3)使用場(chǎng)景

在一個(gè)系統(tǒng)中,要求一個(gè)類有且僅有一個(gè)對(duì)象,如果出現(xiàn)多個(gè)對(duì)象就會(huì)出現(xiàn)“不良反應(yīng)”,可以采用單例模式,具體的場(chǎng)景如下:

  • 要求生成唯一序列號(hào)的環(huán)境;
  • 在整個(gè)項(xiàng)目中需要一個(gè)共享訪問(wèn)點(diǎn)或共享數(shù)據(jù),例如一個(gè)Web頁(yè)面上的計(jì)數(shù)器,可以不用把每次刷新都記錄到數(shù)據(jù)庫(kù)中,使用單例模式保持計(jì)數(shù)器的值,并確保是線程安全的;
  • 創(chuàng)建一個(gè)對(duì)象需要消耗的資源過(guò)多,如要訪問(wèn)IO和數(shù)據(jù)庫(kù)等資源;
  • 需要定義大量的靜態(tài)常量和靜態(tài)方法(如工具類)的環(huán)境,可以采用單例模式(當(dāng)然,也可以直接聲明為static的方式)。

(4)優(yōu)缺點(diǎn)

單例模式的優(yōu)點(diǎn)

  • 由于單例模式在內(nèi)存中只有一個(gè)實(shí)例,減少了內(nèi)存開(kāi)支,特別是一個(gè)對(duì)象需要頻繁地創(chuàng)建銷毀時(shí),而且創(chuàng)建或銷毀時(shí)性能又無(wú)法優(yōu)化,單例模式的優(yōu)勢(shì)就非常明顯;
  • 由于單例模式只生成一個(gè)實(shí)例,所以減少了系統(tǒng)的性能開(kāi)銷,當(dāng)一個(gè)對(duì)象的產(chǎn)生需要比較多的資源的時(shí)候,如讀取配置,產(chǎn)生其他的依賴對(duì)象時(shí),可以通過(guò)在應(yīng)用啟動(dòng)的時(shí)候直接產(chǎn)生一個(gè)單例對(duì)象,然后用永久駐留內(nèi)存的方式來(lái)解決;
  • 單例模式可以避免對(duì)資源的多重占用,例如對(duì)一個(gè)寫文件動(dòng)作,由于只有一個(gè)實(shí)例存在內(nèi)存中,避免對(duì)同一個(gè)資源文件的同時(shí)寫操作;
  • 單例模式可以在系統(tǒng)設(shè)置全局的訪問(wèn)點(diǎn),優(yōu)化和共享資源訪問(wèn),例如可以設(shè)計(jì)一個(gè)單例類,負(fù)責(zé)所有數(shù)據(jù)表的映射處理

單例模式的缺點(diǎn)

  • 單例模式一般沒(méi)有接口,擴(kuò)展困難,若要擴(kuò)展,除了修改代碼基本上沒(méi)有第二種途徑可以實(shí)現(xiàn);
  • 單例模式與單一職責(zé)原則有沖突,一個(gè)類應(yīng)該只實(shí)現(xiàn)一個(gè)邏輯,而不關(guān)心他是否是單例的,是不是要單例取決于環(huán)境,單例模式把“要單例”和業(yè)務(wù)邏輯融合在一個(gè)類中。

2.最基本的實(shí)現(xiàn)方式

代碼實(shí)現(xiàn)為:

public class Singleton {
    private static Singleton singleton;
    // 限制產(chǎn)生多個(gè)對(duì)象
    private Singleton() {
    }
    
    // 獲得對(duì)象實(shí)例的方法
    public static Singleton getSingleton() {
        if(singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

相信大多數(shù)同學(xué)在入門Java的階段都見(jiàn)過(guò)這段代碼。該方式在低并發(fā)的情況下尚不會(huì)出現(xiàn)問(wèn)題,若系統(tǒng)壓力增大,并發(fā)量增加時(shí)則可能在內(nèi)存中出現(xiàn)多個(gè)實(shí)例,破壞設(shè)計(jì)的初衷。本文的后續(xù)就是圍繞這種實(shí)現(xiàn)分析改進(jìn),探討實(shí)現(xiàn)線程安全的單例模式的最佳實(shí)踐。
??為什么這種實(shí)現(xiàn)是線程不安全的呢?如一個(gè)線程A執(zhí)行到singleton = new Singleton();這里,但還沒(méi)有獲得對(duì)象(對(duì)象初始化是需要時(shí)間的),第二個(gè)線程B也在執(zhí)行,執(zhí)行到if(singleton == null)判斷,那么線程B獲得判斷條件也是為真,于是繼續(xù)運(yùn)行下去,線程A獲得了一個(gè)對(duì)象,線程B也獲得了一個(gè)對(duì)象,在內(nèi)存中就出現(xiàn)兩個(gè)對(duì)象,造成單例模式的失效??!
??所以根本原因在于可能存在多個(gè)線程并發(fā)的訪問(wèn)getSingleton()方法造成單例對(duì)象的多次創(chuàng)建,解決因多線程并發(fā)訪問(wèn)導(dǎo)致單例模式實(shí)效的最佳方法就是--不要使用多線程并發(fā)訪問(wèn)。(⊙o⊙)…


3.餓漢式

(1)實(shí)現(xiàn)原理

言歸正傳,上面說(shuō)的問(wèn)題其實(shí)就是對(duì)if(singleton == null)的判斷失效造成singleton = new Singleton();可能會(huì)被多個(gè)線程并發(fā)的執(zhí)行。餓漢式單例模式的實(shí)現(xiàn)的本質(zhì)其實(shí)就是依賴類加載機(jī)制保證構(gòu)造方法只會(huì)被執(zhí)行一次。JVM在類的初始化階段(即在Class被加載后,且被線程使用之前),會(huì)執(zhí)行類的初始化。在執(zhí)行類的初始化期間,JVM會(huì)去獲取一個(gè)鎖。這個(gè)鎖可以同步多個(gè)線程對(duì)同一個(gè)類的初始化。
餓漢式單例的實(shí)現(xiàn)代碼為:

public class Singleton {
    private static Singleton singleton = new Singleton();
    
    private Singleton() {
    }
    
    // 獲得對(duì)象實(shí)例的方法
    public static Singleton getSingleton() {
        return singleton;
    }
}

(2)優(yōu)缺點(diǎn)及適用場(chǎng)景

可以看到餓漢式的實(shí)現(xiàn)非常簡(jiǎn)單,適合那些在初始化時(shí)就要用到單例的情況,如果單例對(duì)象初始化非???,而且占用內(nèi)存非常小的時(shí)候這種方式是比較合適的,可以直接在應(yīng)用啟動(dòng)時(shí)加載并初始化。
不適用的場(chǎng)景:

  • 單例初始化的操作耗時(shí)比較長(zhǎng)而應(yīng)用對(duì)于啟動(dòng)速度又有要求;
  • 單例的占用內(nèi)存比較大;
  • 單例只是在某個(gè)特定場(chǎng)景的情況下才會(huì)被使用,而一般情況下是不會(huì)使用的;

在上述的幾種情況下使用餓漢式的單例模式是不合適的,這時(shí)候就需要用到懶漢式的方式去按需延遲加載單例


4.利用同步鎖機(jī)制實(shí)現(xiàn)的懶漢式

實(shí)現(xiàn)代碼為:

public class Singleton {
    private static Singleton singleton = null;
    
    private Singleton() {
    }
    
    // 獲得對(duì)象實(shí)例的方法
    public static Singleton getSingleton() {
        synchronized(Singleton.class) {
            if(singleton == null) {
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}

這種是最常見(jiàn)的懶漢式單例實(shí)現(xiàn),使用同步鎖synchronized(Singleton.class)防止多線程同時(shí)進(jìn)入造成instance被多次實(shí)例化。但是他的缺陷也是非常明顯的,就是每次在調(diào)用getSingleton()獲取單例的實(shí)例的時(shí)候,都需要進(jìn)行同步。事實(shí)上我們只想保證一次初始化成功,其余的快速返回而已,如果在getInstance頻繁使用的地方就要考慮重新優(yōu)化了。


5.對(duì)同步鎖機(jī)制實(shí)現(xiàn)懶漢式的改進(jìn)--DCL

(1)原理與實(shí)現(xiàn)

由于synchronized(甚至是無(wú)競(jìng)爭(zhēng)的synchronized)存在著巨大的性能開(kāi)銷。因此,人們想出了一個(gè)“聰明”的技巧:雙重檢查鎖定(double-checked locking)。通過(guò)這種方式來(lái)降低同步的開(kāi)銷。下面是使用雙重檢查鎖定來(lái)實(shí)現(xiàn)延遲初始化的代碼實(shí)現(xiàn):

public class Singleton {
    private static Singleton instance = null;                 //1
    
    // 獲得對(duì)象實(shí)例的方法
    public static Singleton getSingleton() {                  //2
        if(instance == null) {                                //3:第一次檢查
            synchronized(Singleton.class) {                   //4:加鎖
                if(instance == null)                          //5:第二次檢查
                    instance = new Singleton();               //6:問(wèn)題的根源產(chǎn)生
            }
        }
        return instance;
    }
        
    private Singleton() {
    }
}

如上面的代碼所示,它的“優(yōu)點(diǎn)”如下:

  • 在多個(gè)線程試圖在同一時(shí)間創(chuàng)建對(duì)象時(shí),會(huì)通過(guò)加鎖來(lái)保證只有一個(gè)線程能創(chuàng)建對(duì)象;
  • 在對(duì)象創(chuàng)建好了以后,執(zhí)行g(shù)etSingleton()方法將不需要獲取鎖,直接返回已經(jīng)創(chuàng)建好的對(duì)象

雙重檢查模式看上去好像很完美,但這是一個(gè)錯(cuò)誤的優(yōu)化,在線程執(zhí)行到上面所示的代碼3處讀取到singleton對(duì)象不為null時(shí),singleton引用的對(duì)象可能還沒(méi)有完成初始化。

(2)問(wèn)題分析

可能產(chǎn)生錯(cuò)誤的場(chǎng)景

  • 1.線程A進(jìn)入getSingleton()方法;
  • 2.因?yàn)榇藭r(shí)instance為null,所以線程A進(jìn)入synchronized塊;
  • 3.線程A執(zhí)行 instance = new Singleton(); 把實(shí)例變量instance設(shè)置成了非空。(注意,是在調(diào)用構(gòu)造方法之前)
  • 4.線程A退出,線程B進(jìn)入。
  • 5.線程B檢查instance是否為空,此時(shí)不為空(第三步的時(shí)候被線程A設(shè)置成了非空)。線程B返回instance的引用。(問(wèn)題出現(xiàn)了,這時(shí)instance的引用并不是Singleton的實(shí)例,因?yàn)闆](méi)有調(diào)用構(gòu)造方法)
  • 6.線程B退出,線程A進(jìn)入;
  • 7.線程A繼續(xù)調(diào)用構(gòu)造方法,完成instance的初始化,再返回。

(3)問(wèn)題根源

多線程問(wèn)題,很大程度是由于非原子性造成的,如果我們每一個(gè)可能產(chǎn)生競(jìng)爭(zhēng)的地方都是原子性的,那多線程需要考慮的東西就要少很多了。在上述程序中也是一樣,我們看第六行代碼:

instance = new Singleton();     //6:問(wèn)題的根源產(chǎn)生

在JMM中,這行代碼可以分解為3個(gè)過(guò)程:

memory = allocate();            //#1為對(duì)象分配內(nèi)存空間
init(memory);                   //#2初始化
instance = memory;              //#3設(shè)置instance,將其指向剛分配的內(nèi)存空間。

上面的3行代碼,如果是順序執(zhí)行,不會(huì)帶來(lái)問(wèn)題。但是,在某些JIT編譯器上,#2和#3可能發(fā)生重排序。也就是說(shuō),重排序后,上面三個(gè)過(guò)程變成了:


memory = allocate();            //#1為對(duì)象分配內(nèi)存空間
instance = memory;              //#2
init(memory);                   //#3初始化

根據(jù)《The Java Language Specification, Java SE 7 Edition》一書中的內(nèi)容:所有線程在執(zhí)行java程序時(shí)必須要遵守intra-thread semantics。intra-thread semantics保證重排序不會(huì)改變單線程內(nèi)的程序執(zhí)行結(jié)果。換句話來(lái)說(shuō),intra-thread semantics允許那些在單線程內(nèi),不會(huì)改變單線程程序執(zhí)行結(jié)果的重排序。上面三行偽代碼的2和3之間雖然被重排序了,但這個(gè)重排序并不會(huì)違反intra-thread semantics。這個(gè)重排序在沒(méi)有改變單線程程序的執(zhí)行結(jié)果的前提下,可以提高程序的執(zhí)行性能。
??下面,再讓我們看看多線程并發(fā)執(zhí)行的時(shí)候的情況。請(qǐng)看下面的示意圖:

這里2和3雖然重排序了,但java內(nèi)存模型的intra-thread semantics將確保2一定會(huì)排在4前面執(zhí)行。因此線程A的intra-thread semantics沒(méi)有改變。但2和3的重排序,將導(dǎo)致線程B在B1處判斷出instance不為空,線程B接下來(lái)將訪問(wèn)instance引用的對(duì)象。此時(shí),線程B將會(huì)訪問(wèn)到一個(gè)還未初始化的對(duì)象。。

分析清楚問(wèn)題發(fā)生的根源之后,可以想出兩個(gè)辦法來(lái)實(shí)現(xiàn)線程安全的延遲初始化:

  • 不允許2和3重排序;
  • 允許2和3重排序,但不允許其他線程“看到”這個(gè)重排序。

后文介紹的解決方案就分別對(duì)應(yīng)于上面這兩點(diǎn)。


6.Java1.5以后安全的DCL版本

(1)實(shí)現(xiàn)代碼

public class Singleton {
    private volatile static Singleton instance = null;
    
    // 獲得對(duì)象實(shí)例的方法
    public static Singleton getSingleton() {
        if(instance == null) {
            synchronized(Singleton.class) {
                if(instance == null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
        
    private Singleton() {
    }
}

(2)原理分析

可以發(fā)現(xiàn)代碼只做一點(diǎn)小的修改(把instance聲明為volatile型),為什么volatile可以解決呢?回顧一下他的兩層語(yǔ)義:

  • (1)可見(jiàn)性:指的是在一個(gè)線程中對(duì)該變量的修改會(huì)馬上由工作內(nèi)存(Work Memory)寫回主內(nèi)存(Main Memory)
  • (2)禁止指令重排序優(yōu)化

當(dāng)聲明對(duì)象的引用為volatile后,“問(wèn)題的根源”的三行偽代碼中的2和3之間的重排序,在多線程環(huán)境中將會(huì)被禁止,從而在根本上解決了問(wèn)題。但是很不幸,禁止指令重排優(yōu)化這條語(yǔ)義直到j(luò)dk1.5以后才能正確工作。此前的JDK中即使將變量聲明為volatile也無(wú)法完全避免重排序所導(dǎo)致的問(wèn)題。所以,在jdk1.5版本前,雙重檢查鎖形式的單例模式是無(wú)法保證線程安全的。
??寫到這里,可能有的同學(xué)會(huì)有疑問(wèn)了:


7.Java1.4以前安全的DCL版本

額(⊙o⊙)…雖然現(xiàn)在的日常開(kāi)發(fā)已經(jīng)普遍在使用Java1.7甚至1.8了。不過(guò)嘗試著探討下在Jav1.4以前實(shí)現(xiàn)安全的DCL還是一個(gè)還有意思的話題。我自己也沒(méi)有找到太確定的答案,這方面的資料也非常的少。下面給出的實(shí)現(xiàn)代碼不一定能保證正確,貼出來(lái)僅供參考,歡迎有興趣的同學(xué)在評(píng)論區(qū)留言分享一下經(jīng)驗(yàn)。

(2)實(shí)現(xiàn)代碼及思路

public class Singleton {
    private static Singleton instance = null;
    // 獲得對(duì)象實(shí)例的方法
    public static Singleton getSingleton() {
        if(instance == null) {                                      //1.第一次檢查
            synchronized(Singleton.class) {                         //2.第一個(gè)synchronized塊
                Singleton temp = instance;                          //3.給臨時(shí)變量temp賦值
                if(temp == null) {                                  //4.第二次檢查
                    synchronized(Singleton.class) {                 //5.第二個(gè)synchronized塊
                        temp = new Singleton();                     //6.解決問(wèn)題的關(guān)鍵地方
                    }
                    instance = temp;                                //7.把temp的引用賦值給instance
                }
            }
        }
        return instance;
    }
    
    private Singleton() {
    }
}

上面給出的代碼中,很關(guān)鍵的地方在于在synchronized塊中引入了一個(gè)臨時(shí)變量Singleton temp,通過(guò)對(duì)temp的判空及相應(yīng)的初始化,保證在代碼7處,執(zhí)行intance = temp;時(shí),instance不為null且完成了初始化


8.內(nèi)部類方式

(1)實(shí)現(xiàn)

在第五部分的結(jié)尾我們提到了兩個(gè)辦法來(lái)實(shí)現(xiàn)線程安全的延遲初始化,內(nèi)部類方式正是基于第二種方法--線程之間重排序透明性
實(shí)現(xiàn)代碼為:

public class Singleton {
    // 獲得對(duì)象實(shí)例的方法
    public static Singleton getSingleton() {
        return SingletonHolder.instance;
    }
    
    /**
     * 靜態(tài)內(nèi)部類與外部類的實(shí)例沒(méi)有綁定關(guān)系,而且只有被調(diào)用時(shí)才會(huì)
     * 加載,從而實(shí)現(xiàn)了延遲加載
     */
    private static class SingletonHolder {
        /**
         * 靜態(tài)初始化器,由JVM來(lái)保證線程安全
         */
        private static Singleton instance = new Singleton();
    }
    
    private Singleton() {
    }
}

(2)原理分析

在這種方式中,使用了一個(gè)專門的內(nèi)部類來(lái)初始化Singleton,JVM將推遲SingletonHolder的初始化操作,直到開(kāi)始使用這個(gè)類時(shí)才初始化。并且在初始化的過(guò)程中JVM會(huì)去獲取一個(gè)用于同步多個(gè)線程對(duì)同一個(gè)類進(jìn)行初始化的鎖,這樣就不需要額外的同步。這種方式不僅能夠保證線程安全,也能保證單例對(duì)象的唯一性,同時(shí)也延遲實(shí)例化,是一種非常推薦的方式。


9.枚舉方式

(1)實(shí)現(xiàn)方法

從Java1.5起,可以通過(guò)使用枚舉機(jī)制來(lái)實(shí)現(xiàn)單例模式:

public enum Singleton {
    // 定義枚舉元素,他就是Singleton的一個(gè)實(shí)例
    INSTANCE;
    
    public void doSomething() {
        // do something
    }
}

調(diào)用方式

Singleton singleton = Singleton.INSTANCE;
singleton.doSomething();

可以看到實(shí)現(xiàn)的代碼非常的簡(jiǎn)潔,按照J(rèn)oshua Bloch大神的原話來(lái)說(shuō):
While this approach has yet to be widely adopted,a single-element enum type is the best way to implement a singleton.

(2)序列化與反序列化的問(wèn)題

在上述的幾種單例模式實(shí)現(xiàn)中,在一個(gè)情況下它們會(huì)出現(xiàn)重新創(chuàng)建對(duì)象的情況,那就是反序列化。
??通過(guò)序列化可以將一個(gè)單例的實(shí)例對(duì)象寫到磁盤,然后再讀回來(lái),從而有效地獲得一個(gè)實(shí)例。即使構(gòu)造函數(shù)是私有的,反序列化時(shí)依然可以通過(guò)特殊的途徑去創(chuàng)建類的一個(gè)新的實(shí)例,相當(dāng)于調(diào)用該類的構(gòu)造函數(shù)。反序列化操作提供一個(gè)很特別的鉤子函數(shù),類中具有一個(gè)私有的、被實(shí)例化的方法readResolve(),這個(gè)方法可以讓開(kāi)發(fā)人員控制對(duì)象的反序列化。例如,上述幾個(gè)實(shí)例中如果要杜絕單例對(duì)象在被反序列化時(shí)重新生成對(duì)象,那么必須加入如下方法:

private Object readResolve() throws ObjectStreamException {
    return INSTANCE;
}

也就是在readResolve方法中將實(shí)例對(duì)象返回,而不是默認(rèn)的重新生成一個(gè)新的對(duì)象。

(3)Java反射攻擊

下面我們基于內(nèi)部類實(shí)現(xiàn)的單例模式的方式,來(lái)演示一下通過(guò)JAVA的反射機(jī)制來(lái)“攻擊”單例模式:

public class TestMain {
    public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        Class<?> classType = Singleton.class;  
        Constructor<?> c = classType.getDeclaredConstructor(null);  
        c.setAccessible(true);  
        Singleton singleton1 = (Singleton) c.newInstance();  
        Singleton singleton2 = Singleton.getSingleton();  
        System.out.println(singleton1 == singleton2);  
    }
}

運(yùn)行結(jié)果:false,可以看到,通過(guò)反射獲取構(gòu)造函數(shù),然后調(diào)用setAccessible(true)就可以調(diào)用私有的構(gòu)造函數(shù),所有singleton1和singleton2是兩個(gè)不同的對(duì)象。如果要抵御這種攻擊,可以修改構(gòu)造器,讓它在被要求創(chuàng)建第二個(gè)實(shí)例的時(shí)候拋出異常。
修改原有代碼為:

public class Singleton {
    
    public static Singleton getSingleton() {
        return SingletonHolder.instance;
    }
    
    private static class SingletonHolder {
        private static Singleton instance = new Singleton();
    }
    
    private static boolean flag = false; 
    
    private Singleton() {
        synchronized(Singleton.class) {
            if(flag == false) {
                flag = !flag;
            } else {
                throw new RuntimeException("單例模式被破壞!");  
            }
        }
    }
}

再次運(yùn)行上面的測(cè)試代碼:得到的結(jié)果為:

Exception in thread "main" java.lang.RuntimeException: 單例模式被破壞!
    at com.danli.Singleton.<init>(Singleton.java:29)
    at com.danli.Singleton.getSingleton(Singleton.java:12)
    at com.danli.TestMain.main(TestMain.java:23)

可以看到,成功的阻止了單例模式被破壞。

但是我們?nèi)绻苯踊诿杜e方式實(shí)現(xiàn)的單例模式進(jìn)行同樣的代碼測(cè)試,會(huì)直接得到結(jié)果:

Exception in thread "main" java.lang.NoSuchMethodException: com.danli.Singleton.<init>()
    at java.lang.Class.getConstructor0(Class.java:2730)
    at java.lang.Class.getDeclaredConstructor(Class.java:2004)
    at com.danli.TestMain.main(TestMain.java:20)

可以看到,枚舉方式實(shí)現(xiàn)的單例自己是可以避免反射攻擊的

(4)枚舉方式的優(yōu)點(diǎn)

餓漢式、懶漢式、雙重校驗(yàn)鎖(DCL)還是靜態(tài)內(nèi)部類都存在的缺點(diǎn):

  • 都需要額外的工作(Serializable、transient、readResolve())來(lái)實(shí)現(xiàn)序列化,否則每次反序列化一個(gè)序列化的對(duì)象實(shí)例時(shí)都會(huì)創(chuàng)建一個(gè)新的實(shí)例。
  • 可能會(huì)有人使用反射強(qiáng)行調(diào)用我們的私有構(gòu)造器(如果要避免這種情況,可以修改構(gòu)造器,讓它在創(chuàng)建第二個(gè)實(shí)例的時(shí)候拋異常)。

枚舉類很好的解決了這兩個(gè)問(wèn)題,使用枚舉除了線程安全和防止反射強(qiáng)行調(diào)用構(gòu)造器之外,還提供了自動(dòng)序列化機(jī)制,防止反序列化的時(shí)候創(chuàng)建新的對(duì)象。因此,《EffectiveJava》Item3中推薦盡可能地使用枚舉來(lái)實(shí)現(xiàn)單例。
??但是在Android中卻不推薦這種用法,在Android官網(wǎng)Manage Your App's Memory中有這樣一段話:

  • Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

意思就是枚舉類這種寫法雖然簡(jiǎn)單方便,但是內(nèi)存占用上是靜態(tài)變量的兩倍以上,所以盡可能的避免這種寫法。
??不過(guò)網(wǎng)上有的建議是如果程序不是大量采用枚舉,那么這種性能的體現(xiàn)是很小的,基本不會(huì)受到影響,不用特別在意。如果程序出現(xiàn)了性能問(wèn)題,理論上這個(gè)地方就是一個(gè)性能優(yōu)化點(diǎn)。


10.單例模式的擴(kuò)展

(1)定義

上文的幾種實(shí)現(xiàn)方式里,一個(gè)類都只產(chǎn)生一個(gè)對(duì)象。萬(wàn)一有天產(chǎn)品提的需求中,需要一個(gè)類只產(chǎn)能產(chǎn)生兩三個(gè)對(duì)象呢?該怎么實(shí)現(xiàn)?

這種需要產(chǎn)生固定數(shù)量對(duì)象的模式就叫做多例模式,實(shí)際上就是單例模式的自然推廣,作為對(duì)象的創(chuàng)建模式,多例模式有以下的特點(diǎn):

  • 多例類可有多個(gè)實(shí)例;
  • 多例類必須自己創(chuàng)建,管理自己的實(shí)例,并向外界提供自己的實(shí)例。

(2)應(yīng)用實(shí)例

喜歡打麻將的同學(xué)(捂臉)都知道,每一桌麻將牌局都需要兩個(gè)骰子,因此骰子就應(yīng)該是多例類,這里就以這個(gè)場(chǎng)景為例來(lái)說(shuō)明多例模式的應(yīng)用。
實(shí)現(xiàn)代碼為:

public class Die {

    private static Die die1 = new Die();
    private static Die die2 = new Die();
    
    private Die() {
        
    }
    
    public static Die getInstance(int whichOne) {
        if(whichOne == 1) {
            return die1;
        } else {
            return die2;
        }
    }

    public synchronized int dice() {
        Random rand = new Random(System.currentTimeMillis());
        int value = rand.nextInt(6);
        value += 1;
        return value;
    }
}

在多例類Die中,使用了餓漢式方式創(chuàng)建了兩個(gè)Die的實(shí)例,根據(jù)靜態(tài)工廠方法的參數(shù),工廠方法返還兩個(gè)實(shí)例中的一個(gè),Die對(duì)象調(diào)用die()方法代表擲骰子,這個(gè)方法會(huì)返還一個(gè)1--6之間的隨機(jī)數(shù),相當(dāng)于骰子的點(diǎn)數(shù)。

(3)實(shí)踐原則

一個(gè)多例類可以使用靜態(tài)變量存儲(chǔ)所有的實(shí)例,特別是實(shí)例數(shù)目不多的時(shí)候,可以使用一個(gè)個(gè)的靜態(tài)變量存儲(chǔ)一個(gè)個(gè)的實(shí)例。當(dāng)數(shù)目較多的時(shí)候,就需要使用Map等集合存儲(chǔ)這些實(shí)例。
??使用這種模式可以讓我們?cè)谠O(shè)計(jì)時(shí)決定在內(nèi)存中有多少個(gè)實(shí)例,方便系統(tǒng)進(jìn)行擴(kuò)展,修正單例可能存在的性能問(wèn)題,提高系統(tǒng)的相應(yīng)速度。例如讀取文件,我們可以在系統(tǒng)啟動(dòng)時(shí)完成初始化工作,在內(nèi)存中啟動(dòng)固定數(shù)量的reader實(shí)例,然后在需要讀取文件時(shí)就可以快速響應(yīng)。


11.小結(jié)

最后總結(jié)一下,不管哪種方案,時(shí)刻牢記單例模式的三大要點(diǎn):

  • 線程安全
  • 延遲加載
  • 序列化與反序列化安全

本文詳細(xì)的分析了懶漢式,餓漢式,雙重檢查鎖定,靜態(tài)內(nèi)部類,枚舉五種方式的具體實(shí)現(xiàn)原理和優(yōu)缺點(diǎn),并簡(jiǎn)要介紹了單例模式的擴(kuò)展--多例模式。希望大家看完之后能對(duì)單例模式有進(jìn)一步的了解,并在日常工作中結(jié)合具體需求選擇適合的單例模式實(shí)現(xiàn)。

參考文獻(xiàn)

1.Double-checked locking and the Singleton pattern
2.雙重檢查鎖定與延遲初始化
3.如何防止單例模式被JAVA反射攻擊
4.Android設(shè)計(jì)模式之單例模式
5.單例模式的一些注意點(diǎn)
6.《設(shè)計(jì)模式之禪》第7章相關(guān)內(nèi)容
7.《Java與模式》第16--18章相關(guān)內(nèi)容
8.《Head First 設(shè)計(jì)模式》第5章相關(guān)內(nè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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 單例模式(SingletonPattern)一般被認(rèn)為是最簡(jiǎn)單、最易理解的設(shè)計(jì)模式,也因?yàn)樗暮?jiǎn)潔易懂,是項(xiàng)目中最...
    成熱了閱讀 4,548評(píng)論 4 34
  • 1 場(chǎng)景問(wèn)題# 1.1 讀取配置文件的內(nèi)容## 考慮這樣一個(gè)應(yīng)用,讀取配置文件的內(nèi)容。 很多應(yīng)用項(xiàng)目,都有與應(yīng)用相...
    七寸知架構(gòu)閱讀 6,981評(píng)論 12 68
  • 前言 本文主要參考 那些年,我們一起寫過(guò)的“單例模式”。 何為單例模式? 顧名思義,單例模式就是保證一個(gè)類僅有一個(gè)...
    tandeneck閱讀 2,636評(píng)論 1 8
  • 1 單例模式的動(dòng)機(jī) 對(duì)于一個(gè)軟件系統(tǒng)的某些類而言,我們無(wú)須創(chuàng)建多個(gè)實(shí)例。舉個(gè)大家都熟知的例子——Windows任務(wù)...
    justCode_閱讀 1,567評(píng)論 2 9
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法,類相關(guān)的語(yǔ)法,內(nèi)部類的語(yǔ)法,繼承相關(guān)的語(yǔ)法,異常的語(yǔ)法,線程的語(yǔ)...
    子非魚_t_閱讀 34,840評(píng)論 18 399

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