【深入设计模式】单例模式—从源码分析内部类单例、枚举单例以及单例模式在框架中的应用
文章目录
- 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)演进