【深入設計模式】單例模式—從源碼分析內(nèi)部類單例离熏、枚舉單例以及單例模式在框架中的應用

@[toc]

前面我們介紹了單例模式的餓漢式和懶漢式寫法荚板,以及從最簡陋的懶漢式到 DCL 版本的演進杰扫,相信你對單例模式已經(jīng)有了很深刻的認識队寇。這一章節(jié)將繼續(xù)介紹另外兩種單例模式的寫法——靜態(tài)內(nèi)部類和枚舉類單例,在介紹完成后從底層代碼剖析這兩種寫法的優(yōu)勢和原理章姓。最后便是單例模式在 JDK 和其他框架下的的源碼以及應用佳遣。


1. 使用靜態(tài)內(nèi)部類實現(xiàn)單例模式

1.1 靜態(tài)內(nèi)部類單例寫法

前面介紹了餓漢式的單例模式確保了線程安全,但是不能夠?qū)崿F(xiàn)延遲加載凡伊;懶漢式能夠確保延遲加載零渐,卻需要確保線程安全。有沒有一種辦法既能夠?qū)崿F(xiàn)延遲加載系忙,又不需要使用同步代碼就能夠保證線程安全的單例呢诵盼?答案是有的,使用靜態(tài)內(nèi)部類的方式來實現(xiàn)單例模式银还。代碼如下:

public class Singleton {
    private Singleton() {
    }

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

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

我們這里使用靜態(tài)內(nèi)部類 SingletonHolder风宁,并將單例成員變量移到該靜態(tài)內(nèi)部類中,獲取單例時直接調(diào)用 SingletonHolder.INSTANCE便可以獲取到該單例蛹疯。靜態(tài)內(nèi)部類與餓漢式的區(qū)別就在于使用了靜態(tài)內(nèi)部類維護對象成員戒财,那么為什么這樣的小改動就能夠即實現(xiàn)懶加載,又是線程安全的呢捺弦?接下來我們對這段代碼進行分析

1.2 如何實現(xiàn)懶加載

首先分析為什么能夠?qū)崿F(xiàn)懶加載饮寞,以下面代碼為例孝扛,Outer 類中有靜態(tài)內(nèi)部類 Inner

public class Outer {

    public static final Outer outer = new Outer();

    static {
        System.out.println("outer static running.");
    }

    public static class Inner {
        public static final Inner inner = new Inner();

        static {
            System.out.println("inner static running.");

        }
    }
}

當我們創(chuàng)建一個內(nèi)部類之后,對該類進行編譯之后將會生成兩個 class 文件 Outer.class 和 Outer$Inner.class 幽崩。也就是說當我進行類加載時實際上需要加載兩個類苦始,下面演示兩種情況:只調(diào)用 outer 對象、只調(diào)用 inner 對象慌申。

// 只調(diào)用 outer 對象
public static void main(String[] args) {
    Outer outer = Outer.outer;
    // 控制臺打印
    // outer static running.
}
// 只調(diào)用 inner 對象
public static void main(String[] args) {
    Outer.Inner inner = Outer.Inner.inner;
    // 控制臺打印
    // inner static running.
}

JVM 中類初始化有這么一個規(guī)定:

遇到new陌选、getstatic、putstatic太示、invokestatic這四條字節(jié)碼指令時柠贤,假設類還沒有進行過初始化。則須要先觸發(fā)其初始化类缤。生成這四條指令最常見的Java代碼場景是:使用new關鍵字實例化對象時臼勉、讀取或設置一個類的靜態(tài)字段(static)時(被static修飾又被final修飾的,已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)餐弱、以及調(diào)用一個類的靜態(tài)方法時

因此從上面可以得出以下結(jié)論:只調(diào)用外部類并且不使用與內(nèi)部類相關的成員變量宴霸、方法時,不會對內(nèi)部類進行初始化膏蚓。而根據(jù) JVM 的規(guī)定瓢谢,當我們在外部內(nèi)調(diào)用內(nèi)部類的成員或方法時才會初始化內(nèi)部類,并且只初始化一次驮瞧。

所以從這里可以看出氓扛,在我們不對內(nèi)部類的靜態(tài)成員、靜態(tài)方法進行調(diào)用時內(nèi)部類時不會進行初始化的论笔。而在內(nèi)部類的單例模式中采郎,在外部類調(diào)用了內(nèi)部類的靜態(tài)成員變量 INSTANCE ,從而觸發(fā)類初始化狂魔,因此確保了懶加載機制蒜埋。

1.3 為什么線程安全

分析了懶加載原因之后再看線程安全就比較簡單了。在對內(nèi)部類進行調(diào)用是內(nèi)部類才會初始化最楷,那么此時和餓漢式一樣會先對靜態(tài)成員進行初始化整份,然后再執(zhí)行調(diào)用方法,在類加載時期完成了單例對象的創(chuàng)建籽孙,因此在獲取的時候就不存在線程安全的問題了烈评。

2. 枚舉類型單例單例模式

2.1 枚舉類型單例寫法

在 《Effective Java》 這本書中推薦使用枚舉類型來獲取單例對象,寫法也非常簡單:

public enum Singleton {
    INSTANCE;

    public Singleton getInstance() {
        return INSTANCE;
    }
}

2.2 枚舉類型單例原理

那么為什么一個簡單的枚舉就能夠保證線程安全的單例呢犯建?我們反編譯一下這段代碼看看編譯之后的類及成員是什么樣的(javap -p Singleton.class)

Compiled from "Singleton.java"
public final class com.sk.demo.singleton.Singleton extends java.lang.Enum<com.sk.demo.singleton.Singleton> {
    // 靜態(tài)成員變量
    public static final com.sk.demo.singleton.Singleton INSTANCE;
    private static final com.sk.demo.singleton.Singleton[] $VALUES;
    public static com.sk.demo.singleton.Singleton[] values();
    public static com.sk.demo.singleton.Singleton valueOf(java.lang.String);
    // 私有構造方法
    private com.sk.demo.singleton.Singleton();
    public com.sk.demo.singleton.Singleton getInstance();
    static {};
}

可以看到 enum 類在編譯之后轉(zhuǎn)化成了一個 final 類础倍,并繼承 java.lang.Enum 這個抽象類。在編譯之后的 Singleton 類中胎挎,擁有一個靜態(tài)成員變量 INSTANCE沟启,以及私有構造方法。然后我們看看完整的反編譯(javap -c Singleton.class):

Compiled from "Singleton.java"
public final class com.sk.demo.singleton.Singleton extends java.lang.Enum<com.sk.demo.singleton.Singleton> {
  public static final com.sk.demo.singleton.Singleton INSTANCE;

  public static com.sk.demo.singleton.Singleton[] values();
    Code:
       0: getstatic     #1                  // Field $VALUES:[Lcom/sk/demo/singleton/Singleton;
       3: invokevirtual #2                  // Method "[Lcom/sk/demo/singleton/Singleton;".clone:()Ljava/lang/Object;
       6: checkcast     #3                  // class "[Lcom/sk/demo/singleton/Singleton;"
       9: areturn

  public static com.sk.demo.singleton.Singleton valueOf(java.lang.String);
    Code:
       0: ldc           #4                  // class com/sk/demo/singleton/Singleton
       2: aload_0
       3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/En
um;
       6: checkcast     #4                  // class com/sk/demo/singleton/Singleton
       9: areturn

  public com.sk.demo.singleton.Singleton getInstance();
    Code:
       0: getstatic     #7                  // Field INSTANCE:Lcom/sk/demo/singleton/Singleton;
       3: areturn

  static {};
    Code:
       0: new           #4                  // class com/sk/demo/singleton/Singleton
       3: dup
       4: ldc           #8                  // String INSTANCE
       6: iconst_0
       7: invokespecial #9                  // Method "<init>":(Ljava/lang/String;I)V
      10: putstatic     #7                  // Field INSTANCE:Lcom/sk/demo/singleton/Singleton;
      13: iconst_1
      14: anewarray     #4                  // class com/sk/demo/singleton/Singleton
      17: dup
      18: iconst_0
      19: getstatic     #7                  // Field INSTANCE:Lcom/sk/demo/singleton/Singleton;
      22: aastore
      23: putstatic     #1                  // Field $VALUES:[Lcom/sk/demo/singleton/Singleton;
      26: return
}

從以上反編譯后的指令可以看到在 static{} 中犹菇,對靜態(tài)變量 INSTANCE 進行構造初始化德迹,從反編譯后的代碼分析就能夠看出 enum 對象編譯之后的類使用餓漢式來保證的單例。

2.3 枚舉類型單例模式的優(yōu)勢

相比于前面的幾種方式揭芍,枚舉類型還有一個好處就是能夠防止反射導致單例失效胳搞。前面幾種辦法都是基于普通類來進行創(chuàng)建、獲取單例對象称杨,若要防止反射破壞單例肌毅,需要單獨進行處理。而 Java 規(guī)定反射不能夠破壞枚舉類型姑原,因此即使使用反射也無法破壞枚舉類型悬而,詳見 java.lang.reflect.Constructor 中的 newInstance 方法。因此枚舉類型的單例是目前最為完美的單例模式寫法了锭汛。

public T newInstance(Object ... initargs)
    throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, null, modifiers);
        }
    }
    // 通過反射類不能夠構造枚舉對象
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ConstructorAccessor ca = constructorAccessor;   // read volatile
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(initargs);
    return inst;
}

3. 單例模式在源碼中的應用

3.1 JDK 中的單例模式

Unsafe 類

在研究多線程時會經(jīng)常到這個類來笨奠,因為 CAS 就是通過 Unsafe 類來實現(xiàn)的。在 Unsafe 類中唤殴,Unsafe 對象也是通過單例模式獲取般婆。下面從源碼中省略多余代碼,提取出來單例模式部分朵逝∥蹬郏可以看到 Unsafe 構造方法被標記為 private,使用靜態(tài)成員變量 theUnsafe 聲明單例對象配名,并在靜態(tài)代碼塊中進行初始化啤咽,從這里可以看出這是一個標準的餓漢式單例。

public final class Unsafe {
    private static final Unsafe theUnsafe;
    
    private Unsafe() {
    }
    
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

    static {
        registerNatives();
        Reflection.registerMethodsToFilter(Unsafe.class, new String[]{"getUnsafe"});
        theUnsafe = new Unsafe();
        // 省略多余代碼
    }

Runtime 類

同樣的段誊,再看 Runtime 類也是一個標準的餓漢式單例

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
}

3.2 Spring 中的單例模式

Spring 的 bean 默認就是單例的對象闰蚕,但是在 Spring 中是通過 ConcurrentHashMap 存放對象,并使用三級緩存來確保單例连舍,雖然與我們所講的單例模式都不太一樣没陡,但是從效果和意義上來講這也是單例模式。Spring 對 Bean 的管理可以參考以下文章:

Spring源碼分析——Bean創(chuàng)建

Spring源碼分析——獲取Bean

Spring源碼分析——解決循環(huán)依賴

3.3 slf4j 中的單例模式

在 slf4j 中的 LoggerFactory 類中也使用了單例模式索赏。在該類中通過 getILoggerFactory() 方法獲取 LoggerFactory 對象盼玄,從下面的源碼中可以看到,getILoggerFactory() 方法使用的是 DCL 來獲取的單例對象潜腻。


public final class LoggerFactory {
    
    private LoggerFactory() {
    }
    
    public static ILoggerFactory getILoggerFactory() {
        if (INITIALIZATION_STATE == 0) {
            Class var0 = LoggerFactory.class;
            synchronized(LoggerFactory.class) {
                if (INITIALIZATION_STATE == 0) {
                    INITIALIZATION_STATE = 1;
                    performInitialization();
                }
            }
        }
        // 省略多余代碼
    }
}

在 slf4j 中的 StaticLoggerBinder 類同樣也使用到了單例模式埃儿,從下面源碼中可以看到 StaticLoggerBinder 也是使用的餓漢式單例模式。


public class StaticLoggerBinder implements LoggerFactoryBinder {
    private static StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
    
    private StaticLoggerBinder() {
        this.defaultLoggerContext.setName("default");
    }
    
    public static StaticLoggerBinder getSingleton() {
        return SINGLETON;
    }

4. 單例模式總結(jié)

  • 單例模式確保了調(diào)用者獲取到的對象始終是同一個

  • 單例模式有餓漢式融涣、懶漢式(DCL)童番、靜態(tài)內(nèi)部類精钮、枚舉等多種寫法,其中枚舉類型是最完美的

  • 枚舉類型單例是指也是餓漢式剃斧,但是枚舉可以防止反射攻擊

  • 單例模式是非常重要的設計模式轨香,并且從源碼可以看出單例模式的使用也是非常廣泛

5. 相關參考

【深入設計模式】單例模式—你確定你會寫單例?餓漢式和懶漢式(DCL)演進

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末幼东,一起剝皮案震驚了整個濱河市臂容,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌根蟹,老刑警劉巖脓杉,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異简逮,居然都是意外死亡球散,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進店門买决,熙熙樓的掌柜王于貴愁眉苦臉地迎上來沛婴,“玉大人,你說我怎么就攤上這事督赤∴业疲” “怎么了?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵躲舌,是天一觀的道長丑婿。 經(jīng)常有香客問我,道長没卸,這世上最難降的妖魔是什么羹奉? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮约计,結(jié)果婚禮上诀拭,老公的妹妹穿的比我還像新娘。我一直安慰自己煤蚌,他們只是感情好耕挨,可當我...
    茶點故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著尉桩,像睡著了一般筒占。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蜘犁,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天翰苫,我揣著相機與錄音,去河邊找鬼这橙。 笑死奏窑,一個胖子當著我的面吹牛导披,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播良哲,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼盛卡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了筑凫?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤并村,失蹤者是張志新(化名)和其女友劉穎巍实,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體哩牍,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡棚潦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了膝昆。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丸边。...
    茶點故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖荚孵,靈堂內(nèi)的尸體忽然破棺而出妹窖,到底是詐尸還是另有隱情,我是刑警寧澤收叶,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布骄呼,位于F島的核電站,受9級特大地震影響判没,放射性物質(zhì)發(fā)生泄漏蜓萄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一澄峰、第九天 我趴在偏房一處隱蔽的房頂上張望嫉沽。 院中可真熱鬧,春花似錦俏竞、人聲如沸绸硕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽臣咖。三九已至,卻和暖如春漱牵,著一層夾襖步出監(jiān)牢的瞬間夺蛇,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工酣胀, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留刁赦,地道東北人娶聘。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像甚脉,于是被迫代替她去往敵國和親丸升。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,585評論 2 359

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