当Kotlin邂逅设计模式之单例模式(一)

2020-02-20 13:40:15 浏览数 (1)

简述: 从这篇文章开始,我将带领大家一起来探讨一下Kotlin眼中的设计模式。说下为什么想着要开始这么一个系列文章。主要基于下面几点原因:

  • 1、设计模式一直是开发者看懂Android源码的一个很大障碍。所以想要理解和运用源码中一些设计思想和技巧,首先看懂源码是第一步,而看懂源码,又得需要设计模式和数据结构算法作为基础,否则看起来云里雾里,只能死记硬背别人总结的结论,最终还是无法消化和理解运用。
  • 2、Kotlin中设计模式的实现和Java的实现在某些方面还是有很大的差别的,利用Kotlin语言自身的特性实现设计模式比硬生生套用Java中的设计模式实现要更优雅和更高效。当然每个设计模式我会对比Java与Kotlin实现区别,以便理解更加深刻。
  • 3、据了解Kotlin有关设计模式实现系列的文章目前在国内还是比较少的,所以想系统地去写一个有关Kotlin邂逅设计模式的系列文章。

说下最终的目标吧,最终目标是有基础能力在分析的源码时候能够站在一个全局角度去思考,而不是一头扎入茫茫源码中无法自拔迷失自我。后面也会随即出一些有关源码分析的文章。所以请暂时先好好掌握这些基础的工具。

一、介绍

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

二、定义

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

三、基本要求

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

四、使用场景

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

五、UML类图

六、饿汉式单例

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

1、Kotlin实现

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

代码语言:javascript复制
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代码

代码语言:javascript复制
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中都无法私自去创建新的单例对象。

2、Java实现
代码语言:javascript复制
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简单得多得多。

七、线程安全的懒汉式单例

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

1、Kotlin实现
代码语言:javascript复制
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();
2、Java实现
代码语言:javascript复制
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;
    }
}

八、DCL(double check lock)改造懒汉式单例

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

1、Java中DCL实现
代码语言:javascript复制
//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;
    }
}
2、Kotlin中DCL实现

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

代码语言:javascript复制
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代码你就会恍然大悟了:

代码语言:javascript复制
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();
      }
   }
3、Kotlin的lazy属性代理内部实现源码分析
代码语言:javascript复制
//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)
}    
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;

九、静态内部类单例

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

1、Kotlin实现
代码语言:javascript复制
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
    }
}
2、Java实现
代码语言:javascript复制
//使用静态内部单例模式
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;
    }
}

十、枚举单例

其实细心的小伙伴就会观察到上面例子中我都会去实现Serializable接口,并且会去实现readResolve方法。这是为了反序列化会重新创建对象而使得原来的单例对象不再唯一。通过序列化一个单例对象将它写入到磁盘中,然后再从磁盘中读取出来,从而可以获得一个新的实例对象,即使构造器是私有的,反序列化会通过其他特殊途径创建单例类的新实例。然而为了让开发者能够控制反序列化,提供一个特殊的钩子方法那就是readResolve方法,这样一来我们只需要在readResolve直接返回原来的实例即可,就不会创建新的对象。 枚举单例实现,就是为了防止反序列化,因为我们都知道枚举类反序列化是不会创建新的对象实例的。 Java的序列化机制对枚举类型做了特殊处理,一般来说在序列枚举类型时,只会存储枚举类的引用和枚举常量名称,反序列化的过程中,这些信息被用来在运行时环境中查找存在的枚举类型对象,枚举类型的序列化机制保证只会查找已经存在的枚举类型实例,而不是创建新的实例。

1、Kotlin实现
代码语言:javascript复制
enum class KEnumSingleton {
    INSTANCE;

    fun doSomeThing() {
        println("do some thing")
    }
}
//在Kotlin中调用
fun main(args: Array<String>) {
    KEnumSingleton.INSTANCE.doSomeThing()
}
//在Java中调用
 KEnumSingleton.INSTANCE.doSomeThing();
2、Java实现
代码语言:javascript复制
public enum EnumSingleton {
    INSTANCE;
    public void doSomeThing() {
        System.out.println("do some thing");
    }
}

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

最后补充一点,关于在Kotlin中使用单例模式的建议: 一般大多数情况情况下直接使用object对象表达式即可, 因为它比较简单,生成的字节码也相比于静态内部类那种方式要少得多;如果需要懒汉式加载的话还是比较建议使用Kotlin中的by lazy Compaion Object那种方式。该系列下一篇,将探索设计模式中的代理模式。

0 人点赞