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

1 單例模式的定義

定義:確保某一個(gè)類只有一個(gè)實(shí)例,自行實(shí)例化并且向整個(gè)系統(tǒng)提供這個(gè)實(shí)例。
單例模式的通用類圖如下所示:

單例模式的通用類圖

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

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

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

  1. 提高效率
    當(dāng)一個(gè)對(duì)象需要頻繁的創(chuàng)建和銷毀,并且對(duì)象的創(chuàng)建和銷毀操作性能無(wú)法優(yōu)化。此時(shí),單例模式可以在應(yīng)用啟動(dòng)的時(shí)候就產(chǎn)生一個(gè)實(shí)例,永久駐留內(nèi)存,可以減少系統(tǒng)創(chuàng)建和銷毀實(shí)例的性能開(kāi)銷,非常明顯地提高效率。另外,單例模式在內(nèi)存中只有一個(gè)實(shí)例,可以減少內(nèi)存開(kāi)支。比如,讀取配置等。
  2. 避免對(duì)資源的多重占用
    例如一個(gè)對(duì)文件的寫操作,由于只有一個(gè)實(shí)例,避免對(duì)同一個(gè)資源文件同時(shí)寫。
  3. 在系統(tǒng)設(shè)置全局訪問(wèn)點(diǎn),優(yōu)化和共享資源訪問(wèn)。

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

  1. 單例模式一般沒(méi)有接口,擴(kuò)展困難。
  2. 單例測(cè)試對(duì)于測(cè)試是不利的。在并行開(kāi)發(fā)環(huán)境中,如果單例模式?jīng)]有完成,是不能進(jìn)行測(cè)試的。沒(méi)有接口,也不能使用mock的方式虛擬一個(gè)對(duì)象。
  3. 單例模式與單一職責(zé)原則有沖突。一個(gè)類應(yīng)該只實(shí)現(xiàn)一個(gè)邏輯,而不關(guān)心是否是單例的。單例模式把單例和業(yè)務(wù)邏輯融合在一個(gè)類中。

3 單例模式的應(yīng)用場(chǎng)景

在一個(gè)系統(tǒng)中,要求一個(gè)類有且僅有一個(gè)實(shí)例,如果出現(xiàn)多個(gè)實(shí)例就會(huì)出現(xiàn)副作用,可以采用單例模式。具體如下:

  1. 要求生成唯一序列號(hào)的環(huán)境。
  2. 在整個(gè)項(xiàng)目中需要共享一個(gè)訪問(wèn)點(diǎn)或者數(shù)據(jù)。
  3. 創(chuàng)建和銷毀一個(gè)對(duì)象需要消耗的資源過(guò)多,但是又經(jīng)常用到。如,要訪問(wèn)IO和數(shù)據(jù)庫(kù)連接等。
  4. 需要定義大量的靜態(tài)常量和靜態(tài)方法(如工具類)的環(huán)境,可以采用單例模式(也可以直接聲明為static)。
  5. 需要頻繁的進(jìn)行創(chuàng)建和銷毀的對(duì)象。

4 單例模式的最佳實(shí)踐

單例模式比較簡(jiǎn)單,也應(yīng)用廣泛。在Spring中,每個(gè)Bean默認(rèn)是單例的,優(yōu)點(diǎn)是Spring容器可以管理這些Bean的生命周期,決定對(duì)象的創(chuàng)建和銷毀時(shí)機(jī),以及創(chuàng)建和銷毀對(duì)象時(shí)的處理。

5 單例模式常見(jiàn)的實(shí)現(xiàn)方式

單例模式的實(shí)現(xiàn)可以分為兩類:餓漢式(饑漢式)和懶漢式。
餓漢式:在程序啟動(dòng)或單例模式類被加載的時(shí)候,單例模式實(shí)例就已經(jīng)被創(chuàng)建。
懶漢式:當(dāng)程序第一次訪問(wèn)單例模式實(shí)例的時(shí)候才進(jìn)行創(chuàng)建。
以上兩種方式各有優(yōu)點(diǎn)。

  • 如果單例模式實(shí)例在系統(tǒng)中會(huì)被頻繁用到,餓漢式比較好
    優(yōu)點(diǎn):程序啟動(dòng)的時(shí)候已經(jīng)進(jìn)行了實(shí)例化,調(diào)用時(shí)直接返回實(shí)例,速度快,效率高。
    缺點(diǎn):如果實(shí)例使用頻率不高或者幾乎不用,啟動(dòng)的時(shí)候就進(jìn)行實(shí)例化,浪費(fèi)內(nèi)存資源。
  • 如果單例模式實(shí)例在系統(tǒng)中很少用到或者幾乎不會(huì)用到,懶漢式較好
    優(yōu)點(diǎn):如果實(shí)例使用頻率不高或者幾乎不用,啟動(dòng)的時(shí)候就不進(jìn)行實(shí)例化,第一次調(diào)用的時(shí)候進(jìn)行實(shí)例化(lazy-loading),節(jié)約內(nèi)存資源。
    缺點(diǎn):?jiǎn)卫J降膶?shí)例如果被頻繁調(diào)用,影響效率。

5.1 餓漢式常見(jiàn)實(shí)現(xiàn)

5.1.1 推薦的實(shí)現(xiàn)(線程安全)

  1. 靜態(tài)變量初始化(最推薦)
public class Singleton(){
     private static Singleton instance = new Singleton();     
     // 私有構(gòu)造函數(shù)
     private Singleton(){
     }

     public static Singleton getInstance(){
         return instance ;
     } 
}
  1. 靜態(tài)代碼塊初始化(類似于1):
public class Singleton(){
     private static Singleton instance; 
     {
        instance = new Singleton();
     }
     // 私有構(gòu)造函數(shù)
     private Singleton(){
     }

     public static Singleton getInstance(){
         return instance ;
     } 
}
  1. 枚舉類
    單實(shí)例枚舉類SingletonEnum :
public enum Singleton {
    /**
     * 實(shí)例
     */
    INSTANCE;

    private Singleton() {

    }

    /**
     * 業(yè)務(wù)方法
     */
    public void doSomething() {
        //TODO 業(yè)務(wù)代碼
    }
}

使用:

public class SingletonDemo {
    public static void main(String[] args) {
        Singleton singleton = Singleton.INSTANCE;
        singleton.doSomething();
    }
}

默認(rèn)枚舉實(shí)例的創(chuàng)建是線程安全的,但是在枚舉中的其他任何方法由程序員自己負(fù)責(zé)。如果你正在使用實(shí)例方法,那么你需要確保線程安全(如果它影響到其他對(duì)象的狀態(tài)的話)。
傳統(tǒng)單例存在的另外一個(gè)問(wèn)題是一旦你實(shí)現(xiàn)了序列化接口,那么它們不再保持單例。但是枚舉單例,JVM對(duì)序列化有保證。
優(yōu)點(diǎn):有序列化和線程安全的保證,代碼簡(jiǎn)單。

5.2 懶漢式常見(jiàn)實(shí)現(xiàn)

  1. 單次判斷實(shí)例為null
    適合單線程,不適合多線程【線程不安全】
public class Singleton {
    private static Singleton singleton;
    private Singleton() {}
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

這種寫法起到了Lazy Loading的效果,但是只能在單線程下使用。如果在多線程下,一個(gè)線程進(jìn)入了 if (singleton == null) 判斷語(yǔ)句塊,還未來(lái)得及往下執(zhí)行,另一個(gè)線程也通過(guò)了這個(gè)判斷語(yǔ)句,這時(shí)便會(huì)產(chǎn)生多個(gè)實(shí)例。所以在多線程環(huán)境下不可使用這種方式。

  1. 同步方法獲取實(shí)例
    線程安全,但是效率低,不推薦使用。
public class Singleton {
    private static Singleton singleton;
    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

解決上面第1種實(shí)現(xiàn)方式的線程不安全問(wèn)題,做個(gè)線程同步就可以了,于是就對(duì)getInstance()方法進(jìn)行了線程同步。
缺點(diǎn):效率太低。每個(gè)線程在想獲得類的實(shí)例時(shí)候,執(zhí)行g(shù)etInstance()方法都要進(jìn)行同步。而其實(shí)這個(gè)方法只執(zhí)行一次實(shí)例化代碼就夠了,后面的想獲得該類實(shí)例,直接return就行了。方法進(jìn)行同步效率太低要改進(jìn)。

  1. 單次判斷實(shí)例為null,同步代碼塊生成實(shí)例【不推薦使用】
public class Singleton {
    private static Singleton singleton;
    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}

由于第2種實(shí)現(xiàn)方式同步效率太低,所以摒棄同步方法,改為同步產(chǎn)生實(shí)例化的的代碼塊。但是這種同步并不能起到線程同步的作用。跟第1種實(shí)現(xiàn)方式遇到的情形一致。假如一個(gè)線程進(jìn)入了 if (singleton == null) 判斷語(yǔ)句塊,還未來(lái)得及往下執(zhí)行,另一個(gè)線程也通過(guò)了這個(gè)判斷語(yǔ)句,這時(shí)便會(huì)產(chǎn)生多個(gè)實(shí)例。

  1. 雙重檢查【推薦使用】
public class Singleton {
    private static volatile Singleton singleton;
    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

Double-Check概念對(duì)于多線程開(kāi)發(fā)者來(lái)說(shuō)不會(huì)陌生。進(jìn)行兩次 if (singleton == null) 檢查,這樣就可以保證線程安全了。實(shí)例化代碼只用執(zhí)行一次,后面再次訪問(wèn)時(shí),判斷 if (singleton == null),直接return實(shí)例化對(duì)象。
優(yōu)點(diǎn):線程安全;延遲加載;效率較高。

  1. 靜態(tài)內(nèi)部類【推薦使用】
public class Singleton {
    private Singleton() {}

    public static Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }

    private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }
}

這種方式跟餓漢式方式采用的機(jī)制類似,但又有不同。兩者都是采用了類裝載的機(jī)制來(lái)保證初始化實(shí)例時(shí)只有一個(gè)線程。不同的地方在餓漢式方式是只要Singleton類被裝載就會(huì)實(shí)例化,沒(méi)有Lazy-Loading的作用,而靜態(tài)內(nèi)部類方式在Singleton類被裝載時(shí)并不會(huì)立即實(shí)例化,而是在需要實(shí)例化時(shí),調(diào)用getInstance方法,才會(huì)裝載SingletonInstance類,從而完成Singleton的實(shí)例化。
類的靜態(tài)屬性只會(huì)在第一次加載類的時(shí)候初始化,所以在這里,JVM幫助我們保證了線程的安全性,在類進(jìn)行初始化時(shí),別的線程是無(wú)法進(jìn)入的。
優(yōu)點(diǎn):避免了線程不安全,延遲加載,效率高。

6 相關(guān)知識(shí)補(bǔ)充

6.1 關(guān)于延遲初始化(lazy loaded)

原則:“除非絕對(duì)必要,否則就不要延遲初始化”。
延遲初始化是一把雙刃劍,它降低了初始化類或者創(chuàng)建實(shí)例的開(kāi)銷,卻增加了訪問(wèn)被延遲初始化的域的開(kāi)銷,考慮到延遲初始化的域最終需要初始化的開(kāi)銷以及域的訪問(wèn)開(kāi)銷,延遲初始化實(shí)際上降低了性能。

參考

  1. JAVA設(shè)計(jì)模式總結(jié)之23種設(shè)計(jì)模式
  2. 設(shè)計(jì)模式之禪
?著作權(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)容

  • 前言 本文主要參考 那些年,我們一起寫過(guò)的“單例模式”。 何為單例模式? 顧名思義,單例模式就是保證一個(gè)類僅有一個(gè)...
    tandeneck閱讀 2,638評(píng)論 1 8
  • 一.什么是單例模式 單例模式的定義:確保一個(gè)類只有一個(gè)實(shí)例,并提供一個(gè)訪問(wèn)他的全局訪問(wèn)點(diǎn)。單例模式是幾個(gè)設(shè)計(jì)模式中...
    Geeks_Liu閱讀 2,337評(píng)論 0 10
  • 單例設(shè)計(jì)模式全解析 在學(xué)習(xí)設(shè)計(jì)模式時(shí),單例設(shè)計(jì)模式應(yīng)該是學(xué)習(xí)的第一個(gè)設(shè)計(jì)模式,單例設(shè)計(jì)模式也是“公認(rèn)”最簡(jiǎn)單的設(shè)計(jì)...
    WekingZhang閱讀 411評(píng)論 0 1
  • 聲明:原創(chuàng)作品,轉(zhuǎn)載請(qǐng)注明出處http://m.itdecent.cn/p/b99e870f4ce0 有的時(shí)...
    蛇發(fā)女妖閱讀 2,976評(píng)論 9 24
  • 1.蝦開(kāi)背去蝦線,倒入料酒、鹽腌制 2.倒入蛋清、淀粉、十三香、燒烤料腌制5分鐘 3.熱鍋熱油,放入蝦仁炸,炸至兩...
    ericguo閱讀 652評(píng)論 0 0

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