再谈单例模式

Wafer Li ... 2017-05-25 11:58 《Head First 设计模式》笔记
  • DesignPattern
大约 4 分钟

之前提到枚举实现是单例的最佳实现,这毋庸置疑;

不过,对比枚举和静态内部类,好像它们的区别就在于防止了反射攻击;

那么,都『攻击』了,为啥偏偏没事去改你的单例呢?直接获取更有意思的信息不是更好吗?

# 1. 『反射攻击』不是攻击

这里所提到的『反射攻击』的概念,实际上并不是信息安全领域的 『攻击』 的概念;

而是, 通过反射的合理利用,可以令单例失效

那么在日常开发中,最常遇到的反射攻击就是 对象的序列化

当单例需要实现序列化的时候,反序列化过程实际上就是使用 反射 来生成了新的实例。

那么在序列化和反序列化的过程中,单例模式就被破坏掉了。

这时,有人提出可以利用 readResolve() 方法来防止这种事情的发生;

而实际上, 单纯利用 readResolve() 也并不能防止单例被破坏;

《Effective Java 第二版》在 77 条提出:

如果依赖 readResolve() 方法来进行实例控制,带有对象引用类型的所有实例域都必须声明为 transient 的。

否则,那种破釜沉舟式的攻击者,就有可能在 readResolve() 方法运行之前,保护指向反序列化对象的引用。

此时,枚举类型就派上用场了,枚举为了防止这种事情的发生,单独实现了一套序列化和反序列化的机制;

大体就是利用 valueOf() 来进行反序列化,而不是使用普通的序列化机制;

同时,也禁止声明 readResolve()readObject() 这类方法。

# 2. Kotlin 单例是懒加载的

下面是 Kotlin 单例的反编译 Java 代码

public final class Test {
   public static final Test INSTANCE;

   private Test() {
      INSTANCE = (Test)this;
   }

   static {
      new Test();
   }
}
1
2
3
4
5
6
7
8
9
10
11

虽然它和所谓的饿汉式 Java 单例很类似,但是在实际使用中,它是 懒加载 的。

为什么呢?

原因就在于 JVM 类的加载时机;

JVM Specification 中在 准备阶段 (opens new window) 中提出:

explicit initializers for static fields are executed as part of initialization (§5.5), not preparation.

所以,上面的 INSTANCE 的实例化,即 static 块是在类加载的 初始化阶段 进行的;

而对于初始化阶段,JVM Specification 强制规定了有且仅有 5 种情况 (opens new window) 可以触发初始化阶段;

而这 5 种情况,都是你真正使用到类的实例的时候才会出现的;

根据这 5 种情况,再结合 Kotlin object 的单例语法和使用,可以得出有且仅有 2 种情况会导致 object 单例提前进行初始化:

  1. 反射
  2. 调用类中其他的静态变量

对于反射,一个很典型的应用场景就是使用 classpath scanner 进行注解扫描;

不过,JB 的工程师提出,classpath scanner 并不需要反射来实现注解扫描[1]

于是我随便找了一个 classpath scanner: fast-classpath-scanner (opens new window)

经过使用之后,发现即使打印出了单例的信息,但是 JVM 只加载了 main 方法的类,而并没有加载单例。

相关的结果在 这个 gist (opens new window) 中。

单例的名字叫 Test,而 main() 方法类的名字叫 SingletonTest

对于第二种情况,在 Kotlin 中是不存在的。为什么呢?

因为对于 Kotlin object 中,声明值的方法只有 var valconst val 三种;

对于前两种,虽然反编译出来的代码指明这样的确定义了两个静态的值;

var 的情况:

val 的情况:

但是,当你使用的时候,却是通过 INSTANCE 来引用的。

这样无论如何都会导致单例的实例化。

而使用 const val 的确得到了一个 public static 的值;

但是,当你使用的时候,编译器会自动替换为 字面量,不会导致单例加载;

所以,综上所述,Kotlin 中 object 单例是懒加载的。


  1. https://discuss.kotlinlang.org/t/kotlin-singleton-implementation/2853/6?u=omysho ↩︎