【深入设计模式】单例模式—从源码分析内部类单例、枚举单例以及单例模式在框架中的应用

@[toc]

前面我们介绍了单例模式的饿汉式和懒汉式写法,以及从最简陋的懒汉式到 DCL 版本的演进,相信你对单例模式已经有了很深刻的认识。这一章节将继续介绍另外两种单例模式的写法——静态内部类和枚举类单例,在介绍完成后从底层代码剖析这两种写法的优势和原理。最后便是单例模式在 JDK 和其他框架下的的源码以及应用。


1. 使用静态内部类实现单例模式

1.1 静态内部类单例写法

前面介绍了饿汉式的单例模式确保了线程安全,但是不能够实现延迟加载;懒汉式能够确保延迟加载,却需要确保线程安全。有没有一种办法既能够实现延迟加载,又不需要使用同步代码就能够保证线程安全的单例呢?答案是有的,使用静态内部类的方式来实现单例模式。代码如下:

public class Singleton {
    private Singleton() {
    }

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

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

我们这里使用静态内部类 SingletonHolder,并将单例成员变量移到该静态内部类中,获取单例时直接调用 SingletonHolder.INSTANCE便可以获取到该单例。静态内部类与饿汉式的区别就在于使用了静态内部类维护对象成员,那么为什么这样的小改动就能够即实现懒加载,又是线程安全的呢?接下来我们对这段代码进行分析

1.2 如何实现懒加载

首先分析为什么能够实现懒加载,以下面代码为例,Outer 类中有静态内部类 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.");

        }
    }
}

当我们创建一个内部类之后,对该类进行编译之后将会生成两个 class 文件 Outer.class 和 Outer$Inner.class 。也就是说当我进行类加载时实际上需要加载两个类,下面演示两种情况:只调用 outer 对象、只调用 inner 对象。

// 只调用 outer 对象
public static void main(String[] args) {
    Outer outer = Outer.outer;
    // 控制台打印
    // outer static running.
}
// 只调用 inner 对象
public static void main(String[] args) {
    Outer.Inner inner = Outer.Inner.inner;
    // 控制台打印
    // inner static running.
}

JVM 中类初始化有这么一个规定:

遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,假设类还没有进行过初始化。则须要先触发其初始化。生成这四条指令最常见的Java代码场景是:使用new关键字实例化对象时、读取或设置一个类的静态字段(static)时(被static修饰又被final修饰的,已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法时

因此从上面可以得出以下结论:只调用外部类并且不使用与内部类相关的成员变量、方法时,不会对内部类进行初始化。而根据 JVM 的规定,当我们在外部内调用内部类的成员或方法时才会初始化内部类,并且只初始化一次。

所以从这里可以看出,在我们不对内部类的静态成员、静态方法进行调用时内部类时不会进行初始化的。而在内部类的单例模式中,在外部类调用了内部类的静态成员变量 INSTANCE ,从而触发类初始化,因此确保了懒加载机制。

1.3 为什么线程安全

分析了懒加载原因之后再看线程安全就比较简单了。在对内部类进行调用是内部类才会初始化,那么此时和饿汉式一样会先对静态成员进行初始化,然后再执行调用方法,在类加载时期完成了单例对象的创建,因此在获取的时候就不存在线程安全的问题了。

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> {
    // 静态成员变量
    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 类在编译之后转化成了一个 final 类,并继承 java.lang.Enum 这个抽象类。在编译之后的 Singleton 类中,拥有一个静态成员变量 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{} 中,对静态变量 INSTANCE 进行构造初始化,从反编译后的代码分析就能够看出 enum 对象编译之后的类使用饿汉式来保证的单例。

2.3 枚举类型单例模式的优势

相比于前面的几种方式,枚举类型还有一个好处就是能够防止反射导致单例失效。前面几种办法都是基于普通类来进行创建、获取单例对象,若要防止反射破坏单例,需要单独进行处理。而 Java 规定反射不能够破坏枚举类型,因此即使使用反射也无法破坏枚举类型,详见 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 类

在研究多线程时会经常到这个类来,因为 CAS 就是通过 Unsafe 类来实现的。在 Unsafe 类中,Unsafe 对象也是通过单例模式获取。下面从源码中省略多余代码,提取出来单例模式部分。可以看到 Unsafe 构造方法被标记为 private,使用静态成员变量 theUnsafe 声明单例对象,并在静态代码块中进行初始化,从这里可以看出这是一个标准的饿汉式单例。

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创建

Spring源码分析——获取Bean

Spring源码分析——解决循环依赖

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. 单例模式总结

  • 单例模式确保了调用者获取到的对象始终是同一个

  • 单例模式有饿汉式、懒汉式(DCL)、静态内部类、枚举等多种写法,其中枚举类型是最完美的

  • 枚举类型单例是指也是饿汉式,但是枚举可以防止反射攻击

  • 单例模式是非常重要的设计模式,并且从源码可以看出单例模式的使用也是非常广泛

5. 相关参考

【深入设计模式】单例模式—你确定你会写单例?饿汉式和懒汉式(DCL)演进

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容