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

简述: 从这篇文章开始,我将带领你们一块儿来探讨一下Kotlin眼中的设计模式。说下为何想着要开始这么一个系列文章。主要基于下面几点缘由:java

  • 一、设计模式一直是开发者看懂Android源码的一个很大障碍。因此想要理解和运用源码中一些设计思想和技巧,首先看懂源码是第一步,而看懂源码,又得须要设计模式和数据结构算法(个人每周一算法和数据结构文章系列也开始了)做为基础,不然看起来云里雾里,只能死记硬背别人总结的结论,最终仍是没法消化和理解运用。
  • 二、Kotlin中设计模式的实现和Java的实现仍是有很大的差异的,利用Kotlin语言自身的特性实现设计模式比硬生生套用Java中的设计模式实现要更优雅和更高效。固然每一个设计模式我会对比Java与Kotlin实现区别,以便理解更加深入。
  • 三、据了解Kotlin有关设计模式实现的文章目前在国内仍是比较少的,因此想系统地去写一个有关Kotlin邂逅设计模式的系列文章。

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

1、介绍

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

2、定义

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

3、基本要求

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

4、使用场景

通常用于肯定某个类只须要一个实例对象,从而避免中了频繁建立多个对象实例所带来资源和性能开销。例如常见的数据库链接或IO操做等。数组

5、UML类图

6、饿汉式单例

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

一、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中都没法私自去建立新的单例对象。app

二、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简单得多得多。ide

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

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

一、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();
复制代码

二、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实现单例方式。

一、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;
    }
}
复制代码

二、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();
      }
   }
复制代码

三、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)
}    
复制代码

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

  • 问题分析:

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

  • 一、给LazySingleton实例分配内存
  • 二、调用LazySingleton的构造函数,初始化成员字段
  • 三、将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的单例模式。因此引出静态内部类单例实现

一、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
    }
}
复制代码

二、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的序列化机制对枚举类型作了特殊处理,通常来讲在序列枚举类型时,只会存储枚举类的引用和枚举常量名称,反序列化的过程当中,这些信息被用来在运行时环境中查找存在的枚举类型对象,枚举类型的序列化机制保证只会查找已经存在的枚举类型实例,而不是建立新的实例。

一、Kotlin实现

enum class KEnumSingleton {
    INSTANCE;

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

二、Java实现

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

//调用方式
EnumSingleton.INSTANCE.doSomeThing();
复制代码

欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不按期翻译一篇Kotlin国外技术文章。若是你也喜欢Kotlin,欢迎加入咱们~~~

Kotlin系列文章,欢迎查看:

数据结构与算法系列:

Kotlin 原创系列:

Effective Kotlin翻译系列

翻译系列:

实战系列:

相关文章
相关标签/搜索