@[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 {
// 静态成员变量
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 {
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 "":(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)演进