一篇文章詳解單例模式

0. 序言

  • 我們要熟練掌握單例模式保檐。不管是實戰(zhàn)開發(fā)中拒贱,還是面試手寫設(shè)計模式中宛徊,都少不了它。
  • 通過閱讀本篇博文逻澳,你會了解常用的單例模式闸天,單例模式三要素,以及如何保證單例模式的安全性斜做。

1. 定義

  • 保證一個類僅有一個實例苞氮,并提供一個訪問它的全局訪問點。

2. UML類圖

單例模式UML.png

3. 通用代碼(餓漢式)

public class Singleton {

    private static final Singleton sSingleton = new Singleton();

    //限制產(chǎn)生多個對象
    private Singleton() {
    }

    //通過該方法獲得實例對象
    public static Singleton getSingleton() {
        return sSingleton;
    }

    //類中其他方法瓤逼,盡量是static
    public static void doSometing() {

    }
}

4. 三要素(非常重要)

  • 私有構(gòu)造函數(shù)笼吟。
  • 暴露公有靜態(tài)方法返回單例類唯一對象。
  • 在多線程環(huán)境下確保單例類對象有且只有一個霸旗。

5. 優(yōu)點

  • 只有一個實例贷帮,減少內(nèi)存開支。(頻繁創(chuàng)建時)
  • 只生成一個實例诱告,減少系統(tǒng)性能開銷撵枢。(一個對象需要比較多的資源時)
  • 避免對資源的多重占用,以免對同一個資源文件同時操作精居。(比如:寫文件時)
  • 優(yōu)化和共享資源訪問锄禽。(可以在系統(tǒng)設(shè)置全局的訪問點)

6. 缺點

  • 沒有接口,擴展困難箱蟆,只能修改代碼沟绪。疑問:為什么不增加接口呢?因為單例模式要求“自行實例化”空猜,接口對單例模式?jīng)]有意義绽慈。
  • 對測試不利恨旱。并行開發(fā)環(huán)境中,單例模式?jīng)]有完成坝疼,是不能進行測試的搜贤。
  • 單例模式與單一職責(zé)原則有沖突。后者規(guī)定一個類應(yīng)該只實現(xiàn)一個邏輯钝凶,是不是單例取決于環(huán)境仪芒。前者規(guī)定必須是單例而且沒有規(guī)定只能有一個邏輯。

7. 使用場景

  • 當(dāng)要求一個類有且只有一個對象耕陷,出現(xiàn)多個對象就會發(fā)生“不良反應(yīng)”時掂名,比如訪問I/O或者數(shù)據(jù)庫等資源。
  • 整個項目需要一個共享訪問點或者共享數(shù)據(jù)哟沫。
  • 工具類對象饺蔑。

8. 注意事項(一)

在高并發(fā)的情況下,餓漢式不會出現(xiàn)產(chǎn)生多個實例的情況嗜诀,但是懶漢式就要注意線程的同步問題猾警,懶漢式代碼如下:

public class Singleton {

    private static Singleton sSingleton = null;

    //限制產(chǎn)生多個對象
    private Singleton() {
    }

    //通過該方法獲得實例對象
    public static Singleton getSingleton() {
        if (sSingleton == null) {
            sSingleton = new Singleton();
        }
        return sSingleton;
    }
    
}

該單例模式在低并發(fā)的情況下并不會出現(xiàn)問題,若并發(fā)量增大則可能出現(xiàn)多個實例隆敢!為什么會這樣呢发皿?

如一個線程A執(zhí)行到sSingleton = new Singleton(),但是沒有獲得對象(對象初始化是需要時間的)拂蝎,第二個線程B也在執(zhí)行穴墅,執(zhí)行到(sSingleton == null)判斷,那么線程B獲得判斷條件也是真温自,于是繼續(xù)運行下去封救,線程A獲得了一個對象,線程B也獲得了一個對象捣作,在內(nèi)存中就出現(xiàn)兩個對象。

解決線程不安全的方法有很多鹅士,可以在getSingleton方法前加sychronized關(guān)鍵字券躁,也可以在getSingleton方法內(nèi)增加sychronized來實現(xiàn)。

public class Singleton {

    private static Singleton sSingleton = null;

    //限制產(chǎn)生多個對象
    private Singleton() {
    }

    //通過該方法獲得實例對象
    public static sychronized Singleton getSingleton() {
        if (sSingleton == null) {
            sSingleton = new Singleton();
        }
        return sSingleton;
    }
}

優(yōu)點是單例只有在使用時才會被實例化掉盅,在一定程序上節(jié)約了資源也拜;缺點是第一次加載時需要及時進行實例化,反應(yīng)稍慢趾痘,最大的問題是每次調(diào)用getInstance都進行同步慢哈,造成不必要的同步開銷。

所以相比懶漢式永票,更加推薦餓漢式卵贱,當(dāng)然各有利弊滥沫,下文會推薦幾種適用的單例模式,別著急键俱,接著往下看兰绣。

9. 注意事項(二)

除了擔(dān)心高并發(fā)以外,還需要考慮對象的復(fù)制情況编振。

在Java中缀辩,對象默認是不可以被復(fù)制的,若實現(xiàn)Cloneable接口踪央,實現(xiàn)clone方法臀玄,則可以直接通過對象復(fù)制方法創(chuàng)建一個新對象,對象復(fù)制是不用調(diào)用類的構(gòu)造函數(shù)的畅蹂,因此即使是私有的構(gòu)造函數(shù)健无,對象仍然可以被復(fù)制。所以解決該問題的方法就是單例類不要實現(xiàn)Cloneable接口魁莉。

10. 推薦寫法

  • 餓漢式模式
public class Singleton {

    private static final Singleton sSingleton = new Singleton();

    //限制產(chǎn)生多個對象
    private Singleton() {
    }

    //通過該方法獲得實例對象
    public static Singleton getSingleton() {
        return sSingleton;
    }

    //類中其他方法睬涧,盡量是static
    public static void doSometing() {

    }
}

優(yōu)點:類加載時就完成了初始化,所以類加載較慢旗唁,但獲取對象的速度快畦浓。這種方式基于類加載機制,避免了多線程的同步問題检疫。

缺點:不能達到懶加載的效果讶请,如果從始至終未使用過這個實例,則會造成內(nèi)存的浪費屎媳。

推薦場景:單例模式經(jīng)常使用的場景下夺溢,選擇餓漢式。

  • 雙重檢索模式(DCL)
public class Singleton {

    private static Singleton sSingleton = null;

    //限制產(chǎn)生多個對象
    private Singleton() {
    }

    //通過該方法獲得實例對象
    public static Singleton getSingleton() {
        if (sSingleton == null) {
            synchronized (Singleton.class) {
                if (sSingleton == null) {
                    sSingleton = new Singleton();
                }
            }

        }
        return sSingleton;
    }
    
}

亮點:在getSingleton方法中對instance進行了兩次判空:第一層判斷主要是為了避免不必要的同步烛谊,第二層判斷則是為了在Singleton等于null的情況下才創(chuàng)建實例风响。

分析:
sSingleton = new Singleton()這句話大致做了3件事情:
①:給Singleton的實例分配內(nèi)存。
②:調(diào)用Singleton()構(gòu)造函數(shù)丹禀,初始化成員字段状勤。
③:將sSingleton對象指向分配的內(nèi)存空間(此時sSingleton不是null了)
但是由于Java編譯器允許處理器亂序執(zhí)行等原因,執(zhí)行順序可能是1,2,3双泪,還可能是1,3,2.如果是后者持搜,并且在3執(zhí)行完畢、2未執(zhí)行之前焙矛,被切換到線程B上葫盼,這時候sSingleton因為已經(jīng)在線程A內(nèi)執(zhí)行過了第三點,sSingleton已經(jīng)是非空了村斟,所以線程B直接取走sSingleton贫导,再使用時就會出錯抛猫,導(dǎo)致DCL失效。
為了避免這類事情的發(fā)生脱盲,JDK1.5之后調(diào)整了JVM,具體化了volatile關(guān)鍵字邑滨,只需要將sSingleton定義改成private volatile static Singleton sSingleton = null 就可以保證sSingleton對象每次都是從主內(nèi)存中讀取的,保證了可見性和有序性钱反,這樣的話就可以使用DCL掖看。以后會有相關(guān)Java內(nèi)存方面的文章,具體闡述volatile關(guān)鍵字面哥。

優(yōu)點:資源利用率高哎壳,第一次執(zhí)行g(shù)etSingleton時單例對象才會被實例化。
缺點:第一次加載時反應(yīng)稍慢尚卫。

推薦理由:資源利用率高归榕!線程安全!絕大多數(shù)場景下可以保證單例對象的唯一性吱涉。

完整代碼:

public class Singleton {

    private volatile static Singleton sSingleton = null;

    //限制產(chǎn)生多個對象
    private Singleton() {
    }

    //通過該方法獲得實例對象
    public static Singleton getSingleton() {
        if (sSingleton == null) {
            synchronized (Singleton.class) {
                if (sSingleton == null) {
                    sSingleton = new Singleton();
                }
            }

        }
        return sSingleton;
    }
    
}
  • 靜態(tài)內(nèi)部類單例模式(最推薦的)

DCL雖然解決了資源消耗刹泄、多余的同步、線程安全等問題怎爵,但是在高并發(fā)場景比較復(fù)雜的情況下依然會出現(xiàn)失效的問題特石,所以推薦使用靜態(tài)內(nèi)部類單例模式:

public class Singleton {

    //限制產(chǎn)生多個對象
    private Singleton() {
    }

    //通過該方法獲得實例對象
    public static Singleton getInstance() {
        return SingletonHolder.sInstance;
    }

    /**
     * 靜態(tài)內(nèi)部類
     */
    private static class SingletonHolder {
        private static final Singleton sInstance = new Singleton();
    }

}

優(yōu)點:

  1. 懶加載:
    第一次加載Singleton類時并不會初始化sInstance,只有在第一次調(diào)用Singleton的getInstance方法時才會導(dǎo)致sInstance被初始化鳖链。
  2. 線程安全和單例對象唯一性:
    第一次調(diào)用Singleton的getInstance方法時會導(dǎo)致虛擬機加載SingletonHolder類姆蘸,這種方式確保了線程安全,還保證了單例對象的唯一性芙委。

推薦理由:懶加載逞敷、線程安全、單例對象唯一性灌侣。

  • 枚舉模式
public enum Singleton {

    INSTANCE;

    public void doSomething() {
        System.out.println("do sth.");
    }

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

優(yōu)點:默認枚舉實例的創(chuàng)建實線程安全的推捐,并且在任何情況下它都是一個單例,包括序列化侧啼。

推薦理由:寫法簡單玖姑,甚至在反序列化的情況下,依然可以保證單例的唯一性慨菱。

  • 容器模式
public class Singleton {

    private static Map<String, Object> objMap = new HashMap<String, Object>();

    private Singleton() {
    }

    public static void registerService(String key, Object instance) {
        if (!objMap.containsKey(key)) {
            objMap.put(key, instance);
        }
    }

    public static Object getService(String key) {
        return objMap.get(key);
    }

}

分析:在程序的初始,將多種單例類型注入到一個統(tǒng)一的管理類中戴甩,在使用時根據(jù)key獲取對象對應(yīng)類型的對象符喝。這種方式使得我們可以管理多種類型的單例,并且在使用時可以通過統(tǒng)一的接口進行獲取操作甜孤,降低了用戶的使用成本协饲,也對用戶隱藏了具體實現(xiàn)畏腕,降低了耦合度。

11. 特別注意

通過反序列化茉稠,上面幾種單例模式都會出現(xiàn)
重新創(chuàng)建對象的情況描馅,枚舉不包括在內(nèi)。

通過反序列化可以將一個單例的實例對象寫到磁盤而线,然后再讀回來铭污,從而獲得一個實例。及時構(gòu)造函數(shù)是私有的膀篮,反序列化依然可以通過特殊途徑去創(chuàng)建類的一個新的實例嘹狞。

反序列化操作提供了一個很特別的函數(shù),類中具有一個私有的誓竿、被實例化的方法readResolve磅网,通過這個方法可以讓開發(fā)人員控制對象的反序列化。

所以上述幾個示例(不包括枚舉)中如果要杜絕單例對象在被反序列化時重新生成對象筷屡,必須加入以下方法

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

readResolve方法將sInstance對象返回涧偷,而不是默認的重新生成一個新的對象。

12. 后續(xù)

如果大家喜歡這篇文章毙死,歡迎點贊燎潮;如果想看更多 設(shè)計模式 方面的技術(shù),歡迎關(guān)注规哲!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末跟啤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子唉锌,更是在濱河造成了極大的恐慌隅肥,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件袄简,死亡現(xiàn)場離奇詭異腥放,居然都是意外死亡,警方通過查閱死者的電腦和手機绿语,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門秃症,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人吕粹,你說我怎么就攤上這事种柑。” “怎么了匹耕?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵聚请,是天一觀的道長。 經(jīng)常有香客問我,道長驶赏,這世上最難降的妖魔是什么炸卑? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮煤傍,結(jié)果婚禮上盖文,老公的妹妹穿的比我還像新娘。我一直安慰自己蚯姆,他們只是感情好五续,可當(dāng)我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蒋失,像睡著了一般返帕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上篙挽,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天荆萤,我揣著相機與錄音,去河邊找鬼铣卡。 笑死链韭,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的煮落。 我是一名探鬼主播敞峭,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蝉仇!你這毒婦竟也來了旋讹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤轿衔,失蹤者是張志新(化名)和其女友劉穎沉迹,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體害驹,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡鞭呕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了宛官。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片葫松。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖底洗,靈堂內(nèi)的尸體忽然破棺而出腋么,到底是詐尸還是另有隱情,我是刑警寧澤亥揖,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布党晋,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏未玻。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一胡控、第九天 我趴在偏房一處隱蔽的房頂上張望扳剿。 院中可真熱鬧,春花似錦昼激、人聲如沸庇绽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瞧掺。三九已至,卻和暖如春凡傅,著一層夾襖步出監(jiān)牢的瞬間辟狈,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工夏跷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留哼转,地道東北人。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓槽华,卻偏偏與公主長得像壹蔓,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子猫态,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容