5、單例模式(Singleton Pattern)

1. 簡介

??單例模式確保某一個類只有一個實例放案,而且自行實例化并向整個系統(tǒng)提供這個實例姚建,這個類稱為單例類,它提供全局訪問的方法吱殉。

??單例模式的要點有三個:一是某個類只能有一個實例掸冤;二是它必須自行創(chuàng)建這個實例;三是它必須自行向整個系統(tǒng)提供這個實例友雳。單例模式是一種對象創(chuàng)建型模式稿湿。單例模式又名單件模式或單態(tài)模式。

在單例模式的實現(xiàn)過程中押赊,需要注意如下三點:

  • 單例類的構(gòu)造函數(shù)為私有(即無法創(chuàng)建對象)饺藤;
  • 提供一個自身的靜態(tài)私有成員變量;
  • 提供一個公有的靜態(tài)工廠方法流礁。

優(yōu)點:

  • 提供了對唯一實例的受控訪問涕俗。因為單例類封裝了它的唯一實例,所以它可以嚴(yán)格控制客戶怎樣以及何時訪問它神帅,并為設(shè)計及開發(fā)團(tuán)隊提供了共享的概念再姑。
  • 由于在系統(tǒng)內(nèi)存中只存在一個對象,因此可以節(jié)約系統(tǒng)資源找御,對于一些需要頻繁創(chuàng)建和銷毀的對象元镀,單例模式無疑可以提高系統(tǒng)的性能。
  • 允許可變數(shù)目的實例霎桅。我們可以基于單例模式進(jìn)行擴(kuò)展栖疑,使用與單例控制相似的方法來獲得指定個數(shù)的對象實例。

缺點:

  • 由于單例模式中沒有抽象層滔驶,因此單例類的擴(kuò)展有很大的困難遇革。
  • 單例類的職責(zé)過重,在一定程度上違背了“單一職責(zé)原則”。因為單例類既充當(dāng)了工廠角色澳淑,提供了工廠方法比原,同時又充當(dāng)了產(chǎn)品角色,包含一些業(yè)務(wù)方法杠巡,將產(chǎn)品的創(chuàng)建和產(chǎn)品的本身的功能融合到一起量窘。
  • 濫用單例將帶來一些負(fù)面問題,如為了節(jié)省資源將數(shù)據(jù)庫連接池對象設(shè)計為單例類氢拥,可能會導(dǎo)致共享連接池對象的程序過多而出現(xiàn)連接池溢出蚌铜;現(xiàn)在很多面向?qū)ο笳Z言(如Java、C#)的運行環(huán)境都提供了自動垃圾回收的技術(shù)嫩海,因此冬殃,如果實例化的對象長時間不被利用,系統(tǒng)會認(rèn)為它是垃圾叁怪,會自動銷毀并回收資源审葬,下次利用時又將重新實例化,這將導(dǎo)致對象狀態(tài)的丟失奕谭。

2. 單例實現(xiàn)

??常見的單例實現(xiàn)方式有五種:餓漢式涣觉、懶漢式、雙重檢測鎖式血柳、靜態(tài)內(nèi)部類式和枚舉單例官册,根據(jù)不同的業(yè)務(wù)場景選用不同的單例實現(xiàn)方式。

2.1 餓漢式

??線程安全,調(diào)用效率高,但是不能延遲加載难捌。

public class Singleton1 {
    // 私有構(gòu)造膝宁,不可用于對象創(chuàng)建
    private Singleton1() {}

    // 靜態(tài)對象即類對象,全局唯一根吁,類加載時即進(jìn)行初始化员淫,保證單一
    private static Singleton1 single = new Singleton1();

    // 靜態(tài)工廠方法
    public static Singleton1 getInstance() {
        return single;
    }
}

??餓漢式單例在類加載初始化時就創(chuàng)建好一個靜態(tài)的對象供外部使用,除非系統(tǒng)重啟击敌,這個對象不會改變介返,所以本身就是線程安全的。

??Singleton通過將構(gòu)造方法限定為private避免了類在外部被實例化愚争,在同一個虛擬機(jī)范圍內(nèi),Singleton的唯一實例只能通過getInstance()方法訪問(事實上通過Java反射機(jī)制是能夠?qū)嵗瘶?gòu)造方法為private的類的挤聘,那基本上會使所有的Java單例實現(xiàn)失效)轰枝。

2.2 懶漢式

??非線程安全,調(diào)用效率高,可延遲加載。

public class Singleton2 {
    // 私有構(gòu)造
    private Singleton2() {}
    private static Singleton2 single = null;
    public static Singleton2 getInstance() {
        // 多線程并發(fā)破壞單例的設(shè)計原則组去,不安全
        if(single == null){
            single = new Singleton2();
        }
        return single;
    }
}

??該示例在多線程環(huán)境下會產(chǎn)生多個single對象鞍陨,在下面雙重校驗?zāi)J接枰愿倪M(jìn)。

2.3 雙重檢測鎖式(double check)

??雙重校驗鎖DCL(double checked locking)

public class Singleton3 {
    // 私有構(gòu)造
    private Singleton3() {}

    private volatile static Singleton3 single = null;

    public static Singleton3 getInstance() {
        
        // 等同于 synchronized public static Singleton3 getInstance()
        synchronized(Singleton3.class){
          // 注意:里面的判斷是一定要加的,否則出現(xiàn)線程安全問題
            if(single == null){
                single = new Singleton3();
            }
        }
        return single;
    }
}

??在方法上加synchronized同步鎖或是用同步代碼塊對類加同步鎖诚撵,此種方式雖然解決了多個實例對象問題缭裆,但是該方式運行效率卻很低下。

??雙重校驗鎖式是線程安全的寿烟。然而澈驼,在jdk1.5之前的版本,許多JVM對于volatile關(guān)鍵字的實現(xiàn)會導(dǎo)致dcl(double check locking)失效筛武。所以缝其,在JDK1.5以前的DCL是不穩(wěn)定的,有時也可能創(chuàng)建多個實例徘六,在1.5以后開始提供volatile關(guān)鍵字修飾變量來達(dá)到穩(wěn)定效果内边。

??volatile對變量single的修飾必不可少,因為volatile保證了原子性和有序性待锈。如果沒有volatile漠其,則single = new Singleton3();的執(zhí)行很可能會被重排序。

禁止指令重排序(有序性)實例化一個對象其實可以分為三個步驟:

  • (1)分配內(nèi)存空間竿音。
  • (2)初始化對象和屎。
  • (3)將內(nèi)存空間的地址賦值給對應(yīng)的引用。

但是由于操作系統(tǒng)可以對指令進(jìn)行重排序谍失,所以上面的過程也可能會變成如下過程:

  • (1)分配內(nèi)存空間眶俩。
  • (2)將內(nèi)存空間的地址賦值給對應(yīng)的引用。
  • (3)初始化對象快鱼。

??如果是這個流程颠印,多線程環(huán)境下就可能將一個未初始化的對象引用暴露出來,從而導(dǎo)致不可預(yù)料的結(jié)果(如題目的描述抹竹,這里就是因為 instance = new Singleton3(); 不是原子操作线罕,編譯器存在指令重排,從而存在線程1 創(chuàng)建實例后(初始化未完成)窃判,線程2 判斷對象不為空(因為已經(jīng)有了地址钞楼,所以判定為非空)后對其操作,但實際對象仍為空(沒有進(jìn)行初始化)袄琳,造成錯誤)询件。因此,為了防止這個過程的重排序唆樊,我們需要將變量設(shè)置為volatile類型的變量宛琅,volatile的禁止重排序保證了操作的有序性。

??Singleton對象的內(nèi)存可見性 這里由于synchronized鎖的是Singleton.class對象逗旁,而不是Singleton對象嘿辟,所以synchronized只能保證Singleton.class對象的內(nèi)存可見性,但并不能保證Singleton對象的內(nèi)存可見性;這里用volatile聲明Singleton红伦,可以保證Singleton對象的內(nèi)存可見性英古。這一點作用也是非常重要的(如題目的描述,避免因為線程1 創(chuàng)建實例后還只存在自己線程的工作內(nèi)存昙读,未更新到主存召调。線程 2 判斷對象為空,創(chuàng)建實例箕戳,從而存在多實例錯誤)某残。

雙重檢查鎖定與延遲初始化

2.4 靜態(tài)內(nèi)部類

??線程安全,調(diào)用效率高陵吸,可以延遲加載玻墅。

public class Singleton5 {
    // 私有構(gòu)造
    private Singleton5() {}

    // 靜態(tài)內(nèi)部類
    private static class InnerObject{
        private static Singleton5 single = new Singleton6();
    }
    
    public static Singleton5 getInstance() {
        return InnerObject.single;
    }
}

??和餓漢式一樣采用的是classLoader機(jī)制,保證了線程安全問題壮虫,但不同的是澳厢,靜態(tài)內(nèi)部類同樣滿足懶加載(當(dāng)調(diào)用getInsstance()方法時,實例才會被創(chuàng)建)囚似,靜態(tài)內(nèi)部類即使Singleton類被加載也不會創(chuàng)建單例對象剩拢,除非調(diào)用里面的getInstance()方法。因為當(dāng)Singleton類被加載時其靜態(tài)內(nèi)部類SingletonHolder沒有被主動使用饶唤。只有當(dāng)調(diào)用getInstance方法時徐伐,才會裝載SingletonHolder類,從而實例化單例對象。

2.5 枚舉單例

??線程安全募狂,調(diào)用效率高办素,不能延遲加載,可以天然的防止反射和反序列化調(diào)用祸穷。單例的枚舉實現(xiàn)在《Effective Java》中有提到性穿,因為其功能完整、使用簡潔雷滚、無償?shù)靥峁┝诵蛄谢瘷C(jī)制需曾、在面對復(fù)雜的序列化或者反射攻擊時仍然可以絕對防止多次實例化等優(yōu)點,單元素的枚舉類型被作者認(rèn)為是實現(xiàn)Singleton的最佳方法祈远。

??不可能出現(xiàn)序列化呆万、反射產(chǎn)生對象的漏洞,但是不能做到延遲加載车份,默認(rèn)的枚舉實例的創(chuàng)建是線程安全的谋减。

枚舉單例模式,有三個好處:

  • 1.實例的創(chuàng)建線程安全,確保單例
  • 2.防止被反射創(chuàng)建多個實例
  • 3.沒有序列化的問題

枚舉類:

public enum DataSourceEnum {
    DATASOURCE;
    private DBConnection connection = null;
    private DataSourceEnum() {
        connection = new DBConnection();
    }
    public DBConnection getConnection() {
        return connection;
    }
}  

客戶端調(diào)用示例:

 public class Main {
    public static void main(String[] args) {
        DBConnection con1 = DataSourceEnum.DATASOURCE.getConnection();
        DBConnection con2 = DataSourceEnum.DATASOURCE.getConnection();
        System.out.println(con1 == con2);
    }
}

返回true

把上面枚舉編譯后的字節(jié)碼反編譯躬充,得到的代碼如下:

public final class DataSourceEnum extends Enum<DataSourceEnum> {
      public static final DataSourceEnum DATASOURCE;
      public static DataSourceEnum[] values();
      public static DataSourceEnum valueOf(String s);
      static {};
}

線程安全問題:

  • DATASOURCE 被聲明為 static 的逃顶,由類加載過程,可以知道虛擬機(jī)會保證一個類的<clinit>() 方法在多線程環(huán)境中被正確的加鎖充甚、同步以政。所以,枚舉實現(xiàn)是在實例化時是線程安全伴找。

序列化問題:

  • Java規(guī)范中規(guī)定盈蛮,每一個枚舉類型極其定義的枚舉變量在JVM中都是唯一的,因此在枚舉類型的序列化和反序列化上技矮,Java做了特殊的規(guī)定抖誉。
  • 在序列化的時候Java僅僅是將枚舉對象的name屬性輸出到結(jié)果中,反序列化的時候則是通過 java.lang.Enum 的 valueOf() 方法來根據(jù)名字查找枚舉對象衰倦。
  • 也就是說袒炉,以下面枚舉為例,序列化的時候只將 DATASOURCE 這個名稱輸出樊零,反序列化的時候再通過這個名稱我磁,查找對于的枚舉類型,因此反序列化后的實例也會和之前被序列化的對象實例相同驻襟。

2.6 破壞單例模式的方法及解決辦法

1夺艰、除枚舉方式外,其他方法都會通過反射的方式破壞單例,反射是通過調(diào)用構(gòu)造方法生成新的對象,所以如果我們想要阻止單例破壞沉衣,可以在構(gòu)造方法中進(jìn)行判斷郁副,若已有實例,則阻止生成新的實例,解決辦法如下

    private SingletonObject1() {
        if (instance != null) {
            throw new RuntimeException("\"實例已經(jīng)存在豌习,請通過 getInstance()方法獲取\"");
        }
    }

2存谎、如果單例類實現(xiàn)了序列化接口Serializable,就可以通過反序列化破壞單例,所以我們可以不實現(xiàn)序列化接口,如果非得實現(xiàn)序列化接口斑鸦,可以重寫反序列化方法readResolve(),反序列化時直接返回相關(guān)單例對象愕贡。

    public Object readResolve() throws ObjectStreamException {
        return instance;
    }

參考:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市巷屿,隨后出現(xiàn)的幾起案子固以,更是在濱河造成了極大的恐慌,老刑警劉巖嘱巾,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件憨琳,死亡現(xiàn)場離奇詭異,居然都是意外死亡旬昭,警方通過查閱死者的電腦和手機(jī)篙螟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來问拘,“玉大人遍略,你說我怎么就攤上這事惧所。” “怎么了绪杏?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵下愈,是天一觀的道長。 經(jīng)常有香客問我蕾久,道長势似,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任僧著,我火速辦了婚禮履因,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘盹愚。我一直安慰自己栅迄,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布皆怕。 她就那樣靜靜地躺著霞篡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪端逼。 梳的紋絲不亂的頭發(fā)上朗兵,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天,我揣著相機(jī)與錄音顶滩,去河邊找鬼余掖。 笑死,一個胖子當(dāng)著我的面吹牛礁鲁,可吹牛的內(nèi)容都是我干的盐欺。 我是一名探鬼主播,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼仅醇,長吁一口氣:“原來是場噩夢啊……” “哼冗美!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起析二,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤粉洼,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后叶摄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體属韧,經(jīng)...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡怨酝,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年薄嫡,在試婚紗的時候發(fā)現(xiàn)自己被綠了霎槐。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盐数。...
    茶點故事閱讀 39,773評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖错洁,靈堂內(nèi)的尸體忽然破棺而出映穗,到底是詐尸還是另有隱情荆烈,我是刑警寧澤,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布裸燎,位于F島的核電站奔滑,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏顺少。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一王浴、第九天 我趴在偏房一處隱蔽的房頂上張望脆炎。 院中可真熱鬧,春花似錦氓辣、人聲如沸秒裕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽几蜻。三九已至,卻和暖如春体斩,著一層夾襖步出監(jiān)牢的瞬間梭稚,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工絮吵, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留弧烤,地道東北人。 一個月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓蹬敲,卻偏偏與公主長得像暇昂,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子伴嗡,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,689評論 2 354