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

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

文章目录

  • 1. 使用静态部类实现单例模式
    • 1.1 静态内部类单例写法
    • 1.2 如何实现懒加载
    • 1.3 为什么线程安全
  • 2. 枚举类型单例单例模式
    • 2.1 枚举类型单例写法
    • 2.2 枚举类型单例原理
    • 2.3 枚举类型单例模式的优势
  • 3. 单例模式在源码中的应用
    • 3.1 JDK 中的单例模式
      • Unsafe 类
      • Runtime 类
    • 3.2 Spring 中的单例模式
    • 3.3 slf4j 中的单例模式
  • 4. 单例模式总结
  • 5. 相关参考

前面我们介绍了单例模式的饿汉式和懒汉式写法,以及从最简陋的懒汉式到 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/Singleton2: aload_03: invokestatic  #5    // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;6: checkcast     #4    // class com/sk/demo/singleton/Singleton9: 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/Singleton3: dup4: ldc    #8    // String INSTANCE6: iconst_07: invokespecial #9    // Method "":(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 Runtime are instance     * methods and must be invoked with respect to the current runtime object.     *     * @return  the Runtime 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)演进