java單例模式,其中的細節(jié)你注意到了嗎

簡介

Singleton UML

單例模式是應(yīng)用最廣的模式之一,它是為了確保某一個類在一個java虛擬機(進程)中有且只有一個實例存在.

帶來的效益:

  1. 能夠?qū)崿F(xiàn)資源共享,避免由于資源操作時導致的性能或損耗.
  2. 能夠?qū)崿F(xiàn)資源調(diào)度,方便資源之間的互相通信.
  3. 控制實例產(chǎn)生的數(shù)量,達到節(jié)約資源的目的.

缺陷 :

  1. 擴展性差,單例一般沒有接口,要擴展只能修改單例類的代碼.
  2. 避免在單例中持有生命周期比單例對象短的引用,容易引起內(nèi)存泄漏.如Android中的Context對象,需要使用 Application Context代替.

下面介紹單例的七種經(jīng)典實現(xiàn)方法.

餓漢模式

public class Singleton {
    // 靜態(tài)變量初始化, 由于靜態(tài)變量在類加載過程中,就會被初始化,且類加載又jvm保證線程安全.
    // 所以這種方式 是線程安全的
    private final static Singleton mInstance = new Singleton();

    // 構(gòu)造函數(shù)私有化
    private Singleton() {
        // 判斷存在則拋出異常, 為了避免反射調(diào)用,產(chǎn)生多個實例
        if (mInstance != null)
            throw new RuntimeException("instance exist");
    }

    public static Singleton getInstance() {
        return mInstance;
    }
}

餓漢模式將變量聲明為靜態(tài),將在Singleton類被加載的時候,在cinit階段進行創(chuàng)建對象,并且是線程安全的, 類加載過程由JVM來保證線程安全.

餓漢模式能否達到懶加載

我們知道餓漢模式的對象實例是在類加載(初始化階段)的過程就被創(chuàng)建了,并且并不是所有的類都是在程序啟動的時候就加載進內(nèi)存,那么一個類在什么情況下會被加載或者初始化呢?

這在虛擬機規(guī)范中是有嚴格規(guī)定的员凝,虛擬機規(guī)范指明 有且只有 五種情況必須立即對類進行初始化:

1 ) 遇到new阻课、getstaticputstaticinvokestatic這四條字節(jié)碼指令

2 ) 使用java.lang.reflect包的方法對類進行反射調(diào)用的時候,如果類沒有進行過初始化典鸡,則需要先觸發(fā)其初始化。

3 ) 當初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進行過初始化,則需要先觸發(fā)其父類的初始化脓豪。

4 ) 當虛擬機啟動時,用戶需要指定一個要執(zhí)行的主類忌卤,虛擬機會先初始化這個主類扫夜。

5 ) 當使用jdk1.7動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結(jié)果,REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄驰徊,并且這個方法句柄所對應(yīng)的類沒有進行初始化笤闯,則需要先出觸發(fā)其初始化。

--------- 引用自 <<深入理解Java虛擬機:JVM高級特性與最佳實踐>>

在這五種情況中,其中2,3,4,5 在單例模式中,幾乎不會遇到,這里暫不討論.
我們來看第一種情況,提到的指令分別對應(yīng)以下操作:

  1. 外部使用 new 創(chuàng)建該類的對象實例
  2. 類中的靜態(tài)變量被外部讀取或者設(shè)置
  3. 外部調(diào)用了 該類的靜態(tài)方法

其中1, 我們把構(gòu)造函數(shù)設(shè)置為 私有(需要提防 反射和反序列化),進本上不會產(chǎn)生問題.

對于2, 我們盡量要避免把變量(除單例變量外)設(shè)為靜態(tài)且非私有(除非你確定在做什么,不然很可能出現(xiàn)內(nèi)存浪費或者內(nèi)存泄漏,畢竟靜態(tài)變量生命周期和程序一樣長).如果外部調(diào)用這樣的類變量,將會觸發(fā)改類初始化.

注釋: 靜態(tài)常量(final static修飾基礎(chǔ)類型變量)的調(diào)用不會觸發(fā)類的加載, 該常量會被加入被調(diào)用類的常量池中

對于3, 我們單例如果提供靜態(tài)方法供外部使用,該靜態(tài)方法被調(diào)用時,將也會進行單例類初始化.但是 靜態(tài)方法,只能調(diào)用static變量,參數(shù)變量,以及局部變量,而靜態(tài)變量在單例中基本上只有 單例本身會聲明為靜態(tài)變量, 總結(jié)起來就是, 這個靜態(tài)方法基本只能達到 工具方法的作用,最好不要聲明在單例中.

結(jié)論: 餓漢模式不能嚴格上實現(xiàn)懶加載,除非嚴格按照要求,不在單例中申明無關(guān)的靜態(tài)變量和靜態(tài)方法,將也能達到 懶加載的效果.

懶漢模式(線程不安全)

public class Singleton {
    private static Singleton sInstance = null;

    private Singleton() {
        // 防止反射調(diào)用,被創(chuàng)建出多個實例
        if (sInstance != null)
            throw new RuntimeException("instance exist");
    }
    
    // 調(diào)用時創(chuàng)建
    public static Singleton getInstance() {
        if (sInstance == null)
            sInstance = new Singleton();
        return sInstance;
    }
}

這種方式能實現(xiàn)懶加載的目的,并且沒有加鎖操作,因此線程不安全,減少了資源的消耗.

單線程模型下,推薦這種方式的單例, 在多線程模式下 強烈不推薦.

懶漢模式(線程安全)

public class Singleton {
    private static Singleton sInstance = null;

    private Singleton() {
        // 防止反射調(diào)用,被創(chuàng)建出多個實例
        if (sInstance != null)
            throw new RuntimeException("instance exist");
    }

    // 調(diào)用時創(chuàng)建
    public synchronized static Singleton getInstance() {
        if (sInstance == null)
            sInstance = new Singleton();
        return sInstance;
    }
}

這種方式這種方式能夠達到 懶加載線程安全,但是 它鎖住了 整個getInstance()方法,

對于讀的操作if (sInstance == null),也進行了加鎖,這樣對性能有一定的影響.

因此,不大推薦這種方式.

DCL 雙重檢查鎖模式

public class Singleton {
    // 聲明為 volatile 是為了避免在多線程中,new對象時,指令重排,
    // 造成對象未創(chuàng)建,而判斷為非空的情況
    private volatile static Singleton sInstance = null;

    private Singleton() {
        // 防止反射調(diào)用,被創(chuàng)建出多個實例
        if (sInstance != null)
            throw new RuntimeException("instance exist");
    }

    public synchronized static Singleton getInstance() {
        // 不加鎖,判斷是否為空, 在鎖競爭的情況下,提高性能
        if (sInstance == null) {
            // 只有當為空的時候,加鎖創(chuàng)建
            synchronized (Singleton.class) {
                if (sInstance == null)
                    sInstance = new Singleton();
            }
        }
        return sInstance;
    }
}

這種方式這種方式能夠達到 懶加載線程安全, 并且沒有懶漢模式模式的缺點.它只對''(即new對象)操作進行加鎖,判斷是否為空時,線程無需等待.

這里需要注意, sInstance必須聲明為 volatile,不然達不到線程安全. 對象的創(chuàng)建可以拆分為 三條指令,如果對其指令重排就可能出現(xiàn)線程不安全的情況. 具體可以參考筆者的另一篇文章 深入理解 java volatile

因此,比較推薦這種寫法.

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

public class Singleton {
    private Singleton() {
        // 防止反射調(diào)用,被創(chuàng)建出多個實例
        if (SingletonHolder.sInstance != null)
            throw new RuntimeException("instance exist");
    }

    // 當該靜態(tài)方法被第一次調(diào)用時,SingletonHolder類被加載到內(nèi)存,
    // 此時,其sInstance變量將會被創(chuàng)建,類加載由jvm保證線程安全
    public static Singleton getInstance() {
        return SingletonHolder.sInstance;
    }

    // 類加載時初始化,達到懶加載的目的.
    // 調(diào)用時才被創(chuàng)建
    private static class SingletonHolder {
        private final static Singleton sInstance = new Singleton();
    }
}

這種方式這種方式能夠達到 懶加載線程安全.

能夠?qū)崿F(xiàn)懶加載,是因為,不管Singleton中存不存在其他靜態(tài)變量或者靜態(tài)方法,都不會影響到 內(nèi)部靜態(tài)類SingletonHolder, 只有當getInstance()方法調(diào)用時,內(nèi)部靜態(tài)類才會被加載,而類加載時,單例被創(chuàng)建實例化. (請對比餓漢模式)

餓漢模式與靜態(tài)內(nèi)部類模式對比

餓漢模式 要對類進行約束也能達到懶加載目的. (不適用多余的靜態(tài)變量和靜態(tài)方法).

靜態(tài)內(nèi)部類模式 不需要進行約束就能達到懶加載目的. 但是需要消耗一個內(nèi)部類的資源來達到目的.(代價很小)

權(quán)衡兩者, 推薦使用靜態(tài)內(nèi)部類方式.

枚舉模式

public enum Singleton {
    INSTANCE;

    public void method() {
        // todo ...
    }
}

上述的單例方式都有兩個致命的缺點, 不能完全保證單例在jvm中保持唯一性.

  1. 反射創(chuàng)建單例對象

解決方案 : 在構(gòu)造上述中判斷,當多于一個實例時,再調(diào)用構(gòu)造函數(shù),直接報錯.

  1. 反序列化時創(chuàng)建對象

解決方案 : 使用readResolve()方法來避免此事發(fā)生.

這兩種缺點雖然都有方式解決,但是不免有些繁瑣.

枚舉類天生有這些特性.而且實現(xiàn)單例相當簡單.

關(guān)于枚舉類型,能夠?qū)崿F(xiàn) 懶加載,線程安全,以及確保單例在jvm中保持唯一性.
請參考筆者的另一篇文章 java 枚舉(enum) 全面解讀

因此, 強力推薦 使用這種方式創(chuàng)建單例.

但是由于枚舉的使用時,枚舉類的裝載和初始化時會有更多的時間和空間的成本, 它的實現(xiàn)比其他方式需要更多的內(nèi)存空間,所以在Android這種受資源約束的設(shè)備中盡量避免使用枚舉單例

總結(jié)

  1. 在單線程下,建議使用懶漢模式(線程不安全版)
  2. 多線程,且資源受限(Android),建議使用DCL靜態(tài)內(nèi)部類版本
  3. 其他情況,建議使用枚舉方法

引用

  1. Android源碼設(shè)計模式
  2. 深入理解java虛擬機_第二版(周志明)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末棍厂,一起剝皮案震驚了整個濱河市望侈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌勋桶,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件侥猬,死亡現(xiàn)場離奇詭異例驹,居然都是意外死亡,警方通過查閱死者的電腦和手機退唠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門鹃锈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瞧预,你說我怎么就攤上這事屎债〗稣” “怎么了?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵盆驹,是天一觀的道長圆丹。 經(jīng)常有香客問我,道長躯喇,這世上最難降的妖魔是什么辫封? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮廉丽,結(jié)果婚禮上倦微,老公的妹妹穿的比我還像新娘。我一直安慰自己正压,他們只是感情好欣福,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著焦履,像睡著了一般拓劝。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上裁良,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天凿将,我揣著相機與錄音,去河邊找鬼价脾。 笑死牧抵,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的侨把。 我是一名探鬼主播犀变,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼秋柄!你這毒婦竟也來了获枝?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤骇笔,失蹤者是張志新(化名)和其女友劉穎省店,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體笨触,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡懦傍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了芦劣。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片粗俱。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖虚吟,靈堂內(nèi)的尸體忽然破棺而出寸认,到底是詐尸還是另有隱情签财,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布偏塞,位于F島的核電站唱蒸,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏烛愧。R本人自食惡果不足惜油宜,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望怜姿。 院中可真熱鬧慎冤,春花似錦、人聲如沸沧卢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽但狭。三九已至披诗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間立磁,已是汗流浹背呈队。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留唱歧,地道東北人宪摧。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像颅崩,于是被迫代替她去往敵國和親几于。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359