0_dF3Ek-ltwlpauuYE.png
Kotlin Vocabulary:The one and only object

单例模式

单例模式是一个应用广泛的设计模式,详情参考:
单例模式

Kotlin 实现单例有一个优雅的方式 —— 使用 object 关键字

Java 的单例

要创建仅包含一个实例的类,需要将构造函数设为私有,并创建该对象的可公开访问的静态引用。然后提供一个静态方法来检查是否创建了对象。该方法必须返回先前创建的实例或调用构造函数并返回该实例。

  1. public class Singleton{
  2. private static Singleton INSTANCE;
  3. private Singleton(){}
  4. public static Singleton getInstance(){
  5. if (INSTANCE == null){
  6. INSTANCE = new Singleton();
  7. }
  8. return INSTANCE;
  9. }
  10. private int count = 0;
  11. public int count(){ return count++; }
  12. }

上面的代码看起来不错,但是有一个主要问题,这段代码不是线程安全的。多线程同时调用 getInstance() 时,可能破坏单例。原因见 下文中 编译优化带来的有序性问题 一节

😺 并发编程出现 bug 的源头
为解决这一问题,通常使用双重校验锁的形式

  1. public class Singleton{
  2. private static Singleton INSTANCE;
  3. private Singleton(){}
  4. public static Singleton getInstance(){
  5. if (INSTANCE == null) { // Single Checked
  6. synchronized (Singleton.class) {
  7. if (INSTANCE == null) { // Double checked
  8. INSTANCE = new Singleton();
  9. }
  10. }
  11. }
  12. return INSTANCE;
  13. }
  14. private int count = 0;
  15. public int count(){ return count++; }
  16. }

这一过程略微繁琐,因此 Java 中大多数时候使用枚举来创建单例。

Kotlin 的单例

Kotlin 没有静态方法或者静态变量,在 Kotlin 中声明静态方法和变量可以通过伴生对象的形式(compaion object)

  1. class Singleton private constructor() {
  2. private var count = 0
  3. fun count(): Int {
  4. return count++
  5. }
  6. companion object {
  7. private var INSTANCE: Singleton? = null// Double checked
  8. // Single Checked
  9. val instance: Singleton?
  10. get() {
  11. if (INSTANCE == null) { // Single Checked
  12. synchronized(Singleton::class.java) {
  13. if (INSTANCE == null) { // Double checked
  14. INSTANCE =
  15. Singleton()
  16. }
  17. }
  18. }
  19. return INSTANCE
  20. }
  21. }
  22. }

或者我们也可以使用 object 关键字,让代码更简单

  1. object Singleton {
  2. private var count: Int = 0
  3. fun count() {
  4. count++
  5. }
  6. }

compaion object 背后实现

  1. class Test {
  2. companion object {
  3. const val TEST = "TEST"
  4. }
  5. }

要查看 Kotlin 类的字节码,可以选择 Tools > Kotlin > Show Kotlin Bytecode 显示字节码后,点击 Decompile 转换为 Java 代码 111.gif

  1. public final class Test {
  2. @NotNull
  3. public static final String TEST = "TEST";
  4. @NotNull
  5. public static final Test.Companion Companion = new Test.Companion((DefaultConstructorMarker)null);
  6. public static final class Companion {
  7. private Companion() {
  8. }
  9. public Companion(DefaultConstructorMarker $constructor_marker) {
  10. this();
  11. }
  12. }
  13. }

可以看到,compaion object 是使用 静态内部类 Compaion 实现的。因此默认情况下 Java 调用 Kotlin 的代码方式是这样的:Test.Compaion.TEST

object 背后实现

  1. object Singleton {
  2. private var count: Int = 0
  3. fun count() {
  4. count++
  5. }
  6. }

要查看 Kotlin 类的字节码,可以选择 Tools > Kotlin > Show Kotlin Bytecode 显示字节码后,点击 Decompile 转换为 Java 代码 111.gif

  1. public final class Singleton {
  2. private static int count;
  3. @NotNull public static final Singleton INSTANCE;
  4. public final void count() { int var10000 = count++; }
  5. private Singleton() {}
  6. static {
  7. Singleton var0 = new Singleton();
  8. INSTANCE = var0;
  9. }
  10. }

首次访问静态字段时,静态块仅被调用一次。JVM 处理静态块的方式与同步块类似,即使它们没有synchronized 关键字也是如此。在初始化此 Singleton 类时,JVM 会在同步块上获得一个锁,从而使另一个线程无法访问它。释放锁后,已经创建了 Singleton 实例,因此静态块将不再执行。这样可以保证只有一个 Singleton 实例。另外,该对象是线程安全的,并且在首次访问该对象时是延迟创建的。

为什么 static 代码块是线程安全的?

我们都知道,static 代码块只会在类加载时调用一次

那么我们可以做一个推断:如果类加载这个过程是线程安全的即意味着 static 代码块是线程安全的

Java 中类加载依靠 ClassLoader 中的 loadClass 方法 实现的,而该方法是被 synchronized 修饰的
🥁 双亲委托机制

作为验证,我们可以在 loadClass 方法上打入条件断点(过滤我们想要验证的类)
111.gif

为什么 Kotlin 推荐使用 object 实现单例?

object 背后实现的写法有点像饿汉式的单例:

  1. public class Single {
  2. private Single() {
  3. }
  4. private static Single INSTANCE = new Single();
  5. public static Single getInstance() {
  6. return INSTANCE;
  7. }
  8. }

我们都知道 Java 的单例一般都使用懒汉式的双重校验锁的形式,那么为什么 Kotlin 使用的是 饿汉式呢?

这个问题很有意思,严格讲,object 不能等价于懒汉式。

object 中不能加入伴生对象:
image.png

从这个限制我们可以推断出 object 的用法,即当使用 object 时肯定是使用其成员。如果这么说比较抽象的话,我们可以对比一下 Java 饿汉式的写法:

  1. public class Single {
  2. private Single() {
  3. }
  4. + public static void test() {
  5. +
  6. + }
  7. private static Single INSTANCE = new Single();
  8. public static Single getInstance() {
  9. return INSTANCE;
  10. }
  11. }

我们可以在 Single 中加入静态方法 test(),当外部调用 test() 时,Single 实例便被创建出来,即使我们没调用 getInstance()

而 object 禁止使用伴生对象,因此我们在使用 object 类时便意味着调用其成员,因此 object 同懒汉式一样,是能够延迟加载的。

因此我们可以将 object 视为线程安全的懒汉式写法。

结论

  • Kotlin 中可以使用 object 来实现单例
  • compaion object 背后是通过名叫 Compaion 的静态内部类实现的
  • object 背后是使用 java 静态代码块实现的
  • object 背后实现的写法有点像饿汉式,但由于 object 类使用时只能调用它的某个成员,因此它的单例是 延迟加载 的(加载类 -> 执行静态代码块 -> 创建对象)
  • 静态代码块是 线程安全 的,因为 ClassLoader#loadClass() 是线程安全的