服务端相关 / Kotlin 如何实现常用的单例模式

Kotlin 如何实现常用单例模式

从这篇文章开始,我将带领大家一起来进入 Kotlin 实战篇,俗话说光说不练假把式,下面该系列就是阐述 Kotlin 是如何用于平时的开发,这篇文章将带领大家如何使用 Kotlin 实现常见的设计模式,相比 Java 实现设计模式,Kotlin 就会显得简洁和高效没有冗余的模板代码。这篇文章介绍的是最为简单和常用的单利模式。

1. 单例模式的介绍

单例模式是开发者最为常见的一种设计模式,也是 23 种设计模式中最为简单一种设计模式。大部分的开发者都知道它的使用和原理。单例模式顾名思义就是在应用这个模式时,单例对象的类必须是只有一个对象实例存在。在一些应用场景中我们只需要一个全局唯一的对象实例去调度整体行为。还有一些情况为了系统资源开销考虑,避免重复创建多个实例,往往采用单例模式来保证全局只有一个实例对象。

2. 单例模式的定义

保证某个类只有一个实例对象,该实例对象在内部进行实例化,并且提供了一个获取该实例对象的全局访问点。

3. 单例模式的基本要求

  • 1、构造器私有化,private 修饰,主要为了防止外部私自创建该单例类的对象实例
  • 2、提供一个该实例对象全局访问点,在 Java 中一般是以公有的静态方法或者枚举返回单例类对象
  • 3、在多线程环境下保证单例类有且只有一个对象实例,以及在多线程环境下获取单例类对象实例需要保证线程安全。
  • 4、在反序列化时保证单例类有且只有一个对象实例

4. 单例模式的使用场景

一般用于确定某个类只需要一个实例对象,从而避免中了频繁创建多个对象实例所带来资源和性能开销。例如常见的数据库连接或 IO 操作等。

5. 单例模式的 UML 类图

图片描述

6. 饿汉式单例

饿汉式单例模式是实现单例模式比较简单的一种方式,它有个特点就是不管需不需要该单例实例,该实例对象都会被实例化。

6.1 Kotlin 实现饿汉式单例

在 Kotlin 中实现一个饿汉式单例模式可以说是非常非常简单,只需要定义一个 object 对象表达式即可,无需手动去设置构造器私有化和提供全局访问点,这一点 Kotlin 编译器全给你做好了。

object KSingleton : Serializable {//实现Serializable序列化接口,通过私有、被实例化的readResolve方法控制反序列化
    fun doSomething() {
        println("do some thing")
    }

    private fun readResolve(): Any {//防止单例对象在反序列化时重新生成对象
        return KSingleton//由于反序列化时会调用readResolve这个钩子方法,只需要把当前的KSingleton对象返回而不是去创建一个新的对象
    }
}

//在Kotlin中使用KSingleton
fun main(args: Array<String>) {
    KSingleton.doSomething()//像调用静态方法一样,调用单例类中的方法
}
//在Java中使用KSingleton
public class TestMain {
    public static void main(String[] args) {
        KSingleton.INSTANCE.doSomething();//通过拿到KSingleton的公有单例类静态实例INSTANCE, 再通过INSTANCE调用单例类中的方法
    }
}

KSingleton 反编译成 Java 代码

public final class KSingleton implements Serializable {
   public static final KSingleton INSTANCE;

   public final void doSomething() {
      String var1 = "do some thing";
      System.out.println(var1);
   }

   private final Object readResolve() {
      return INSTANCE;//可以看到readResolve方法直接返回了INSTANCE而不是创建新的实例
   }

   static {//静态代码块初始化KSingleton实例,不管有没有使用,只要KSingleton被加载了,
   //静态代码块就会被调用,KSingleton实例就会被创建,并赋值给INSTANCE
      KSingleton var0 = new KSingleton();
      INSTANCE = var0;
   }
}

可能会有人疑问:没有看到构造器私有化,实际上这一点已经在编译器层面做了限制,不管你是在 Java 还是 Kotlin 中都无法私自去创建新的单例对象。

6.2 Java 实现饿汉式单例

public class Singleton implements Serializable {
    private Singleton() {//构造器私有化
    }

    private static final Singleton mInstance = new Singleton();

    public static Singleton getInstance() {//提供公有获取单例对象的函数
        return mInstance;
    }

    //防止单例对象在反序列化时重新生成对象
    private Object readResolve() throws ObjectStreamException {
        return mInstance;
    }
}

对比一下 Kotlin 和 Java 的饿汉式的单例实现发现,是不是觉得 Kotlin 会比 Java 简单得多得多。

7. 线程安全的懒汉式单例

可是有时候我们并不想当类加载的时候就去创建这个单例实例,而是想当我们使用这个实例的时候才去初始化它。于是乎就有了懒汉式的单例模式

7.1 Kotlin 实现线程安全的懒汉式单例

class KLazilySingleton private constructor() : Serializable {
    fun doSomething() {
        println("do some thing")
    }
    companion object {
        private var mInstance: KLazilySingleton? = null
            get() {
                return field ?: KLazilySingleton()
            }

        @JvmStatic
        @Synchronized//添加synchronized同步锁
        fun getInstance(): KLazilySingleton {
            return requireNotNull(mInstance)
        }
    }
    //防止单例对象在反序列化时重新生成对象
    private fun readResolve(): Any {
        return KLazilySingleton.getInstance()
    }
}
//在Kotlin中调用
fun main(args: Array<String>) {
    KLazilySingleton.getInstance().doSomething()
}
//在Java中调用
 KLazilySingleton.getInstance().doSomething();

7.2 Java 实现线程安全的懒汉式单例

class LazilySingleton implements Serializable {
    private static LazilySingleton mInstance;

    private LazilySingleton() {}//构造器私有化

    public static synchronized LazilySingleton getInstance() {//synchronized同步锁保证多线程调用getInstance方法线程安全
        if (mInstance == null){
            mInstance = new LazilySingleton();
        }
        return mInstance;
    }
    
    private Object readResolve() throws ObjectStreamException {//防止反序列化
        return mInstance;
    }
}

8. DCL (double check lock) 改造懒汉式单例

我们知道线程安全的单例模式直接是使用 synchronized 同步锁,锁住 getInstance 方法,每一次调用该方法的时候都得获取锁,但是如果这个单例已经被初始化了,其实按道理就不需要申请同步锁了,直接返回这个单例类实例即可。于是就有了 DCL 实现单例方式。

8.1 Java 中 DCL 实现

//DCL实现单例模式
public class LazySingleTon implements Serializable {
    //静态成员私有化,注意使用volatile关键字,因为会存在DCL失效的问题
    private volatile static LazySingleTon mInstance = null; 

    private LazySingleTon() { //构造器私有化
    }

    //公有获取单例对象的函数
    //DCL(Double Check Lock) 既能在需要的时候初始化单例,又能保证线程安全,且单例对象初始化完后,调用getInstance不需要进行同步锁
    public static LazySingleTon getInstance() {
        if (mInstance == null) {//为了防止单例对象初始化完后,调用getInstance再次重复进行同步锁
            synchronized (LazySingleTon.class) {
                if (mInstance == null) {
                    mInstance = new LazySingleTon();
                }
            }
        }

        return mInstance;
    }

    private Object readResolve() throws ObjectStreamException {
        return mInstance;
    }
}

8.2 Kotlin 中 DCL 实现

在 Kotlin 中有个天然特性可以支持线程安全 DCL 的单例,可以说也是非常非常简单,就仅仅 3 行代码左右,那就是 Companion Object + lazy 属性代理,一起来看下吧。

class KLazilyDCLSingleton private constructor() : Serializable {//private constructor()构造器私有化

    fun doSomething() {
        println("do some thing")
    }

    private fun readResolve(): Any {//防止单例对象在反序列化时重新生成对象
        return instance
    }
    
    companion object {
        //通过@JvmStatic注解,使得在Java中调用instance直接是像调用静态函数一样,
        //类似KLazilyDCLSingleton.getInstance(),如果不加注解,在Java中必须这样调用: KLazilyDCLSingleton.Companion.getInstance().
        @JvmStatic
        //使用lazy属性代理,并指定LazyThreadSafetyMode为SYNCHRONIZED模式保证线程安全
        val instance: KLazilyDCLSingleton by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { KLazilyDCLSingleton() }
    }
}

//在Kotlin中调用,直接通过KLazilyDCLSingleton类名调用instance
fun main(args: Array<String>) {
    KLazilyDCLSingleton.instance.doSomething()
}
//在Java中调用
public class TestMain {
    public static void main(String[] args) {
    //加了@JvmStatic注解后,可以直接KLazilyDCLSingleton.getInstance(),不会打破Java中调用习惯,和Java调用方式一样。
       KLazilyDCLSingleton.getInstance().doSomething();
       //没有加@JvmStatic注解,只能这样通过Companion调用
       KLazilyDCLSingleton.Companion.getInstance().doSomething();
    }
}

注意:建议上面例子中添加 @JvmStatic 注解,Kotlin 这门语言可谓是操碎了心,做的很小心翼翼,为了不让 Java 开发者打破他们的调用习惯,让调用根本无法感知到是 Kotlin 编写,因为外部调用方式和 Java 方式一样。如果硬生生把 Companion 对象暴露给 Java 开发者他们可能会感到一脸懵逼。

可能大家对 lazy 和 Companion Object 功能强大感到一脸懵,让我们一起瞅瞅反编译后的 Java 代码你就会恍然大悟了:

public final class KLazilyDCLSingleton implements Serializable {
   @NotNull
   private static final Lazy instance$delegate;
   //Companion提供公有全局访问点,KLazilyDCLSingleton.Companion实际上一个饿汉式的单例模式
   public static final KLazilyDCLSingleton.Companion Companion = new KLazilyDCLSingleton.Companion((DefaultConstructorMarker)null);
   public final void doSomething() {
      String var1 = "do some thing";
      System.out.println(var1);
   }

   private final Object readResolve() {
      return Companion.getInstance();
   }

   private KLazilyDCLSingleton() {
   }

   static {//注意: 可以看到静态代码块中并不是初始化KLazilyDCLSingleton的instance而是初始化它的Lazy代理对象,说明KLazilyDCLSingleton类被加载了,
   //但是KLazilyDCLSingleton的instance并没有被初始化,符合懒加载规则,那么什么时候初始化instance这就涉及到了属性代理知识了,下面会做详细分析
      instance$delegate = LazyKt.lazy(LazyThreadSafetyMode.SYNCHRONIZED, (Function0)null.INSTANCE);
   }

   // $FF: synthetic method
   public KLazilyDCLSingleton(DefaultConstructorMarker $constructor_marker) {
      this();
   }

   @NotNull
   public static final KLazilyDCLSingleton getInstance() {
      return Companion.getInstance();//这里可以看到加了@JvmStatic注解后,getInstance内部把我们省略Companion.getInstance()这一步,这样一来Java调用者就直接KLazilyDCLSingleton.getInstance()获取单例实例
   }

   //Companion静态内部类实际上也是一个单例模式
   public static final class Companion {
      // $FF: synthetic field
      static final KProperty[] ?delegatedProperties = new KProperty[]{(KProperty)Reflection.property1(new PropertyReference1Impl(Reflection.getOrCreateKotlinClass(KLazilyDCLSingleton.Companion.class), "instance", "getInstance()Lcom/mikyou/design_pattern/singleton/kts/KLazilyDCLSingleton;"))};

      /** @deprecated */
      // $FF: synthetic method
      @JvmStatic
      public static void instance$annotations() {
      }

      @NotNull
      //这个方法需要注意,最终instance初始化和获取将在这里进行
      public final KLazilyDCLSingleton getInstance() {
         //拿到代理对象
         Lazy var1 = KLazilyDCLSingleton.instance$delegate;
         KProperty var3 = ?delegatedProperties[0];
         //代理对象的getValue方法就是初始化instance和获取instance的入口。内部会判断instance是否被初始化过没有就会返回新创建的对象,
         //初始化过直接返回上一次初始化的对象。所以只有真正调用getInstance方法需要这个实例的时候instance才会被初始化。
         return (KLazilyDCLSingleton)var1.getValue();
      }

      private Companion() {//Companion构造器私有化
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }

8.3 Kotlin 的 lazy 属性代理内部实现源码分析

//expect关键字标记这个函数是平台相关,我们需要找到对应的actual关键字实现表示平台中一个相关实现 
public expect fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T>

//对应多平台中一个平台相关实现lazy函数
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {//根据不同mode,返回不同的Lazy的实现,我们重点看下SynchronizedLazyImpl
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE//为了解决DCL带来指令重排序导致主存和工作内存数据不一致的问题,这里使用Volatile原语注解。具体Volatile为什么能解决这样的问题请接着看后面的分析
    private val lock = lock ?: this

    override val value: T
        get() {//当外部调用value值,get访问器会被调用
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {//进行第一层的Check, 如果这个值已经初始化过了,直接返回_v1,避免走下面synchronized获取同步锁带来不必要资源开销。
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {//进行第二层的Check,主要是为了_v2被初始化直接返回
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                //如果没有初始化执行initializer!!() lambda, 
                //实际上相当于执行外部调用传入的 by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { KLazilyDCLSingleton() } 中的KLazilyDCLSingleton()也即是返回KLazilyDCLSingleton实例对象
                    val typedValue  initializer!!()
                    _value = typedValue//并把这个实例对象保存在_value中
                    initializer = null
                    typedValue
                }
            }
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

    private fun writeReplace(): Any = InitializedLazyImpl(value)
}

8.4 DCL 存在多线程安全问题分析及解决

DCL 存在多线程安全问题,我们都知道线程安全主要来自主存和工作内存数据不一致以及重排序 (指令重排序或编译器重排序造成的)。那么 DCL 存在什么问题呢?
首先,mInstance = new LazySingleton() 不是一个原子操作而是分为三步进行:

  • 1、给 LazySingleton 实例分配内存;
  • 2、调用 LazySingleton 的构造函数,初始化成员字段;
  • 3、将 mInstance 对象引用指向分配的内存空间 (此时 mInstance 不为 null)。

在 JDK1.5 之前版本的 Java 内存模型中,Cache, 寄存器到主存回写顺序规则,无法保证第 2 和第 3 执行的顺序,可能是 1-2-3,也有可能是 1-3-2

若 A 线程先执行了第 1 步,第 3 步,此时切换到 B 线程,由于 A 线程中已经执行了第 3 步所以 mInstance 不为 null,那么 B 线程中直接把 mInstance 取走,由于并没有执行第 2 步使用的时候就会报错。

为了解决该问题,JDK1.5 之后,具体化了 volatile 关键字,能够确保每次都是从主存获取最新有效值。所以需要 private volatile static LazySingleTon mInstance = null;

9. 静态内部类单例

DCL 虽然在一定程度上能解决资源消耗、多余 synchronized 同步、线程安全等问题,但是某些情况下还会存在 DCL 失效问题,尽管在 JDK1.5 之后通过具体化 volatile 原语来解决 DCL 失效问题,但是它始终并不是优雅一种解决方式,在多线程环境下一般不推荐 DCL 的单例模式。所以引出静态内部类单例实现

9.1 Kotlin 实现静态内部类单例

class KOptimizeSingleton private constructor(): Serializable {//private constructor()构造器私有化
    companion object {
        @JvmStatic
        fun getInstance(): KOptimizeSingleton {//全局访问点
            return SingletonHolder.mInstance
        }
    }

    fun doSomething() {
        println("do some thing")
    }
    
    private object SingletonHolder {//静态内部类
        val mInstance: KOptimizeSingleton = KOptimizeSingleton()
    }
    
    private fun readResolve(): Any {//防止单例对象在反序列化时重新生成对象
        return SingletonHolder.mInstance
    }
}

9.2 Java 实现静态内部类单例

//使用静态内部单例模式
public class OptimizeSingleton implements Serializable {
    //构造器私有化
    private OptimizeSingleton() {
    }

    //静态私有内部类
    private static class SingletonHolder {
        private static final OptimizeSingleton sInstance = new OptimizeSingleton();
    }

    //公有获取单例对象的函数
    public static OptimizeSingleton getInstance() {
        return SingletonHolder.sInstance;
    }
    
    public void doSomeThings() {
        System.out.println("do some things");
    }
    
    //防止反序列化重新创建对象
    private Object readResolve() {
        return SingletonHolder.sInstance;
    }
}

10. 枚举单例

其实细心的小伙伴就会观察到上面例子中我都会去实现 Serializable 接口,并且会去实现 readResolve 方法。这是为了反序列化会重新创建对象而使得原来的单例对象不再唯一。

通过序列化一个单例对象将它写入到磁盘中,然后再从磁盘中读取出来,从而可以获得一个新的实例对象,即使构造器是私有的,反序列化会通过其他特殊途径创建单例类的新实例。然而为了让开发者能够控制反序列化,提供一个特殊的钩子方法那就是 readResolve 方法,这样一来我们只需要在 readResolve 直接返回原来的实例即可,就不会创建新的对象。

枚举单例实现,就是为了防止反序列化,因为我们都知道枚举类反序列化是不会创建新的对象实例的。 Java 的序列化机制对枚举类型做了特殊处理,一般来说在序列枚举类型时,只会存储枚举类的引用和枚举常量名称,反序列化的过程中,这些信息被用来在运行时环境中查找存在的枚举类型对象,枚举类型的序列化机制保证只会查找已经存在的枚举类型实例,而不是创建新的实例。

10.1 Kotlin 实现枚举单例

enum class KEnumSingleton {
    INSTANCE;

    fun doSomeThing() {
        println("do some thing")
    }
}
//在Kotlin中调用
fun main(args: Array<String>) {
    KEnumSingleton.INSTANCE.doSomeThing()
}
//在Java中调用
 KEnumSingleton.INSTANCE.doSomeThing();

10.2 Java 实现枚举单例

public enum EnumSingleton {
    INSTANCE;
    public void doSomeThing() {
        System.out.println("do some thing");
    }
}

//调用方式
EnumSingleton.INSTANCE.doSomeThing();

11. 总结

到这里有关 Kotlin 实现常见的单例模式就阐述完毕,这篇文章通过与 Java 对比,并用几种常见单例模式举例对比 Kotlin 和 Java 实现。下篇文章将继续设计模式中代理模式,看看 Kotlin 为代理模式带来什么神奇的魔法。