Kotlin 设计模式系列之单例模式

写在前面

前段时间在回顾 Java 当中的 23(泛指并不是只有23) 种设计模式,最近又在学习 Kotlin ,而后,便萌生了一个想法,是否是能够把二者结合起来,考虑到我是那种学完就忘的人,那就经过写笔记的形式把学习过程记录下来,加深印象,可是个人自制力又比较差,难以坚持下去,那就再经过一个系列文章分享的方式督促本身吧。java

因而,一个 Kotlin 的设计模式系列文章的 Flag 就这么立下来了。这是本系列文章的第一篇,目前计划是在保证质量的前提下,一周至少要完成一篇,挑战也比较大,实际上也是边学边写。是否是和美剧的风格比较像(边拍边写)。既然这样,做为第一篇必须是个大杀器。android

因此,本篇文章内容仍是比较多的,可是强烈建议你耐心看完,相信你会对单例模式有一个很清晰的认识,也欢迎互相交流学习。固然,若是你以为内容确实挺不错的,记得点赞并关注哦。git

好了,话很少说,接下来进入正文吧。github

单例模式介绍

单例模式是一个比较简单的设计模式,同时也是挺有意思的一个模式,虽然看起来简单,可是能够玩出各类花样。好比 Java 当中的懒饿汉式单例等。数据库

什么是单例

单例模式的定义:编程

Ensure a class only has one instance, and provide a global point of access to it.设计模式

简单来讲,确保某一个类只有一个实例,且自行实例化并向整个系统提供。缓存

单例模式的适用场景

  • 提供一个全局的访问,且只要求一个实例,如应用的配置信息
  • 建立一个对象比较耗费资源,如数据库链接管理、文件管理、日志管理等
  • 资源共享,如线程池
  • 工具类对象(也能够直接使用静态常量或者静态方法)
  • 要求一个类只能产生两三个实例对象,好比某些场景下,会要求两个版本的网络库实例,如公司内网和外网的网络库实例

单例模式的简单实现

Java 当中实现一个简单的单例:安全

public class Singleton {

	    private static Singleton sInstance;

	    /**
	     * 构造方法私有化
	     */
	    private Singleton() {}

	    /**
	     * 提供获取惟一实例的方法
	     * @return 实例
	     */
	    public static Singleton getInstance() {
	        if (sInstance == null) {
	            sInstance = new Singleton();
	        }
	        return sInstance;
	    }

	}
复制代码

优秀的单例模式设计

上面的单例模式实现简单,但会存在一些问题,好比它并非一个线程安全的。一般在设计一个优秀的单例会参考如下 3 点:网络

  • 延迟加载(懒加载)
  • 线程安全
  • 防止反射破坏

Java 中的单例模式回顾

刚才简单实现的单例就是延迟加载,即懒汉式,由于只有在调用 getInstance() 方法的时候才会去初始化实例,可是,同时也是线程不安全的,缘由是在多线程的场景下,假如一个线程执行了 if (sInstance == null),而建立对象是须要花费一些时间的,这时另外一个线程也进入了 if (sInstance == null) 里并执行了 代码,这样,就会有两个实例被建立出来,而这显然并非单例所指望的。

咱们看下通过改良后的懒汉式。

1. 懒汉式改良版-线程安全

public class Singleton {

	    private static Singleton sInstance;
	    
	    private Singleton() {}

	    public static synchronized Singleton getInstance() {
	        if (sInstance == null) {
	            sInstance = new Singleton();
	        }
	        return sInstance;
	    }

	}
复制代码

该版本的缺点显而易见,虽然实现了延迟加载,可是对方法添加了同步锁,性能影响很大,因此这种方式不推荐使用。

2. 懒汉式增强版-线程安全

public class Singleton {

	    private volatile static Singleton sInstance;

	    private Singleton() {}

	    public static Singleton getInstance() {
	        if (sInstance == null) {
	            synchronized (Singleton.class) {
	                if (sInstance == null) {
	                    sInstance = new Singleton();
	                }
	            }
	        }
	        return sInstance;
	    }

	}
复制代码

这里使用了双重检查机制,也就是执行了两次 if (sInstance == null) 判断,便是延迟加载,又保证了线程安全,并且性能也不错。

虽然这种方式可使用,可是代码量多了不少,也变得更复杂,我一开始理解起来就以为特别费劲。

因此,这里也对两次 if (sInstance == null) 简单作下说明:

第一次 if (sInstance == null) ,其实在多线程场景下,是并不起做用的,重要的中间的同步锁以及第二次 if (sInstance == null),好比一个线程进入了第一次 if (sInstance == null),接着执行到了同步代码块,这时另外一个线程也经过了第一个 if (sInstance == null),也来到了同步代码块,假设若是没有第二次 if (sInstance == null),那第一个线程执行完同步代码块,接着第二个线程也会执行同步代码块,这样就会有两个实例被建立出来,可是若是同步代码块里面加上第二次的 if (sInstance == null) 的检测。第二个线程执行的时候,就不会再去建立实例了,由于第一个线程已经执行并建立完了实例。这样,双重检测就很好避免了这种状况。

3. 饿汉式

public class Singleton {

	    private static Singleton sInstance = new Singleton();

	    private Singleton() {}

	    public static Singleton getInstance() {
	        return sInstance;
	    }

	}
复制代码

简单直接,由于在类初始化的过程当中,会执行静态代码块以及初始化静态域来完成实例的建立,而该初始化过程是由 JVM 来保证线程安全的。至于缺点嘛,由于类被初始化的时机有多种方式,而对于单例来讲,若是不是经过调用 getInstance() 初始化,也就形成了必定的资源浪费。不过,这种方式也是可使用的。

4. 静态内部类

public class Singleton {

	    public static Singleton getInstance() {
	        return SingletonInstance.sInstance;
	    }

	    private static class SingletonInstance {
	        private static final Singleton sInstance = new Singleton();
	    }

	}
复制代码

这种方式也比较容易理解,饿汉式是利用了类初始化的过程,会执行静态代码块以及初始化静态域来完成实例的建立,而静态内部类的方式是利用了 Singleton 类初始化的时候,可是并不会去初始化 SingletonInstance 静态内部类,而是只有在你去调用了 getInstance()方法的时候,才会去初始化 SingletonInstance 静态内部类,并建立 Singleton 的实例,很巧妙的一种方式。

饿汉式和静态内部类的方式都是利用了 JVM 帮助咱们保证了线程的安全性,由于类的静态属性会在第一次类初始化的时候执行,而在执行类的初始化时,别的线程是没法进入的。

推荐使用静态内部类的方式,这种方式应该是目前使用最多的一种,同时具有了延迟加载、线程安全、效率高三个优势。

好了,回顾完咱们 Java 当中的花式玩单例,咱们再对照下以前优秀单例设计的 3 点要求,是否是延迟加载和线程安全这两点已经没有问题了。不过第三点,防止反射破坏好像尚未说到呢。各位能够先思考下,等说完 Kotlin 的单例模式后,咱们再一块儿来看这个问题。

Kotlin 中的单例模式

终于到了本文的重点,码点字不容易啊,Kotlin 做为一个一样面向 JVM 的静态编程语言,它的单例模式又是如何的呢。

咱们先想下,首先,刚才 Java 中的单例大部分都是经过一个静态属性的方式实现,那在 Kotlin 当中是否是也能够经过一样的方式呢。

做为一个刚入门不久的 Kotlin 菜鸟,能够比较明确的告诉你,在 Kotlin 当中是没有 Java 的静态方法和静态属性这样的一个直接概念。因此,对于一开始从 Java 切换到 Kotlin 的开发仍是有些不太习惯。不过,相似的静态方法和属性的机制仍是有的,感兴趣的同窗能够去看下 Kotlin 的官方文档,这里就不展开了。

因此,理论上来讲,你能够彻底按照 Java 的方式在 Kotlin 中把单例也花式玩一遍。不过,若是仅仅只是这样,那这篇文章应该就不叫 Kotlin 单例模式分析了,而是 Java 单例模式分析。

因此,咱们来看下 Kotlin 官方文档描述的单例是如何写的:

object Singletons {

	}
复制代码

我擦,有没有感受到起飞,一个关键字 object 就搞定单例了,什么懒汉式、饿汉式仍是其余式...通通闪一边去!

咱们接着看下官方的说明:

Singleton may be useful in several cases, and Kotlin (after Scala) makes it easy to declare singletons, This is called an object declaration, and it always has a name following the object keyword.Object declaration's initialization is thread-safe.

在 Kotlin 当中直接经过关键字 object 声明一个单例,而且它是线程安全的。

另外,还有一个很重要的一句话:

object declarations are initialized lazily, when accessed for the first time;

同时,也意味着 object 声明的方式也是延迟加载。

有同窗可能会好奇了,它是怎么实现的呢?

很简单,咱们能够经过 Android Studio 把上面的代码转成咱们比较容易理解的 Java 代码再看下:

public final class Singletons {
	   public static final Singletons INSTANCE;

	   static {
	      Singletons var0 = new Singletons();
	      INSTANCE = var0;
	   }
	}
复制代码

在类初始化的时候执行静态代码块来建立实例,本质上和上面的饿汉式没有任何区别嘛,看到这里,你们应该明白过来了,这并非什么延迟加载嘛,顶多也就一个语法糖而已。

但是官网上明明说的是 lazily 延迟加载,一开始我对这里也是感到很困惑。不过,由于这是 Kotlin,仍是有它的一些特别之处的。咱们来简单回顾和梳理一下类的初始化,以前,咱们提过类的初始化是在特定的时机才会发生,那到底是哪些时机呢?

  • 建立一个类的实例的时候,如 User user = new User()
  • 调用一个类中的静态方法,如 User.create()
  • 给类或者接口中声明的静态属性赋值时,如 User.sCount = 10
  • 访问类或者接口声明的静态属性,如 int count = User.sCount
  • 经过发射也会形成类的初始化
  • 顶层类中执行 assert 语句

这里,咱们主要关心第 二、三、4 条所说的静态相关时机所发生的类初始化,回到以前的问题,为何 Kotlin 说 object 声明的是延迟加载呢,其实能够换个角度来理解,首先,当一个类没有被初始化的时候,也就是实例没有建立的时候,那么,咱们均可以认为它是延迟加载。而在 Kotlin 当中是没有静态方法和属性的这样的一个直接概念,也就是说在 object 声明的单例中没有静态方法和属性的前提下,那么这个类是没有其余时机被初始化的,只有当它被第一次访问的时候,才会去初始化。怎么访问呢,咱们来看代码吧:

object Singletons {

        var name = "I am Kotlin Singletons"

    }

    fun main(args: Array<String>) {
        val singletonsName = Singletons.name
        println(singletonsName)
    }
复制代码

由于 object 声明的属性是能够直接经过类名的方式访问,因此这里猛一看会有点懵。咱们换成 Java 代码就好理解了,看下访问代码:

// val singletonsName = Singletons.name 转换成 Java 代码就是下面的意思
    String singletonsName = Singletons.INSTANCE.getName();
复制代码

也就是说,在咱们第一次访问 object 声明的类中的属性或者方法时,会先触发类的初始化时机,去执行静态代码块中的实例建立,也就是咱们所认为的延迟加载

其实 Kotlin 并无什么所谓的黑科技,它的单例实现原理和 Java 本质上是一致的,只是,在 Kotlin 中对于一些咱们熟知的特性,好比单例,实体类(data 关键字声明)的实现,作了更加规范化的处理,并同时让这些特性的实现代码变得更简单。 而在 Java 当中,对于这些细节,平时写起来可能不会特别去注意,好比在单例中会定义一些静态属性或者静态方法,就会致使一些并不符合咱们预期的结果。

另外,经过刚才转换后的 Java 代码,咱们也能够确认它是线程安全的。

最后,Kotlin 中的 obejct 声明的也是能够继承其余父类。

防止反射破坏的问题

什么是反射破坏?尽管咱们在单例模式经过构造方法私有化,并自行提供了有且只有一个的实例获取方法,可是,这不能防止经过反射机制去访问这个单例类的私有构造方法进行实例化,而且,只要我愿意,我想建立几个实例就建立几个实例。

举个饿汉式的例子:

public class Singleton {

	    private static Singleton sInstance = new Singleton();

	    private Singleton() {}

	    public static Singleton getInstance() {
	        return sInstance;
	    }

	}
	
	/**
	 * 单例反射测试
	 */
	public class SingletonReflection {

	    public static void main(String[] args) {
	        System.out.println("getInstance = " + Singleton.getInstance().hashCode());
	        try {
	            Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
	            constructor.setAccessible(true);
	            Singleton instance = constructor.newInstance();
	            System.out.println("reflection = " + instance.hashCode());
	        } catch (Exception e) {
	            e.printStackTrace();
	        }
	    }

	}


	执行结果:
	getInstance = 1915318863
	reflection = 1283928880
复制代码

咱们在测试代码中,能够看到执行结果中,两个 Singleton 类的实例 hashCode 的值不同,也就是说,咱们经过反射的方式,成功的又建立出了一个实例。

而这也意味着以前说的全部的单例方式均可以经过反射的方式去进行实例化,从而破坏原有的单例模式,固然在 Kotlin 当中也是同样,WTF !

好了,不着急拍桌子,咱们相信办法总比困难多,既然是经过反射访问私有构造参数来建立实例,因此仍是有办法去避免的,继续看代码:

public class Singleton {

	    private static Singleton sInstance = new Singleton();

	    private Singleton() {
	        if (sInstance != null) {
	            throw new RuntimeException("Can not create instance in this class with Singleton");
	        }
	    }

	    public static Singleton getInstance() {
	        return sInstance;
	    }

	}
复制代码

二话不说,咱们向你抛出了一个炸弹,噢不,是异常。

这里,会有另外一个问题,若是是懒加载的单例实现方式,就不能直接经过以上的方式来阻止了。不过,办法仍是有的。这里就不详细说了,感兴趣的同窗的能够去看下这篇文章

考虑到实际场景当中,基本不会有人会这么去作,因此,以前说的单例实现,你们仍是能够愉快使用的。这里的反射破坏也只是让你们有个了解。那假设真的有人这么心血来潮去作了,嗯,直接给丫扔个炸弹!就是这么残暴~

单例的一些扩展

带参数的单例

通常来讲,并不推荐在初始化单例的时候,经过构造方法中传参数,由于若是须要传参数,那就意味着这个单例的对象会根据参数的不一样是有可能变化的。这违反了单例模式的设计初衷。

可是在 Android 当中,咱们写单例的时候,常常会须要持有一个全局 Application Context 对象,好比这句代码 Singleton.getInstance(contenxt).sayHello(),这个时候静态内部类以及 Kotlin 中的 object 声明的方式就都没法知足了。

这里提供两种方式:

  1. 懒汉式增强版

    Java 代码

    public class Singleton {
    
         private static Singleton sInstance;
         
         private Context context
         
         private Singleton(Context context) {
             this.context = context;
         }
    
         public static Singleton getInstance(Context context) {
             if (sInstance == null) {
                 synchronized (Singleton.class) {
                     if (sInstance == null) {
                         sInstance = new Singleton(context);
                     }
                 }
             }
             return sInstance;
         }
    
     }
    复制代码

    Kotlin 代码(参考自 Google Sample 的代码

    @Database(entities = arrayOf(User::class), version = 1)
     abstract class UsersDatabase : RoomDatabase() {
    
         abstract fun userDao(): UserDao
    
         companion object {
    
             @Volatile private var INSTANCE: UsersDatabase? = null
    
             fun getInstance(context: Context): UsersDatabase =
                     INSTANCE ?: synchronized(this) {
                         INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
                     }
    
             private fun buildDatabase(context: Context) =
                     Room.databaseBuilder(context.applicationContext,
                             UsersDatabase::class.java, "Sample.db")
                             .build()
         }
     }
    复制代码
  2. 提供注入方法(我的推荐)

    object Singleton {
    
         private var context: Context? = null
    
         fun init(context: Context?) {
             this.context = context
         }
    
     }
    
     class MainApplication : Application() {
    
         override fun onCreate() {
             super.onCreate()
             Singleton.getInstance().init(this)
         }
    
     }
    复制代码

推荐 2 的理由是由于,通常单例当中持有的 Context 都是全局的,否则持有 ActivityContext 就会形成内存泄漏,因此,在这种场景下,咱们能够在 Application 类中直接经过 Singleton.getInstance().init(context) 去注入一个 Context ,虽然多了一个注入的逻辑,可是好处也很明显,更符合咱们的场景设计,而且在后面的使用中,也不用每次调用这个单例的时候传入 Context 对象

枚举单例

是的,枚举也是单例的一种实现,不过实际使用的场景比较少,这里就很少介绍了,感兴趣的去了解一下。

enum class SingleEnum {
	    INSTANCE
	}
复制代码

另外,Kotlin 中的枚举类有不少种用法,关于这个我再单独写个文章说明一下,若是有时间的话。

哎,我真是太容易给本身立 Flag 了...

多实例单例

什么是多实例单例,就是在某些场景下,咱们对一个类要求有且只有两三个实例对象,一般的作法是在构造单例的时候,传入一个 ID 用来标识某个实例,并存入到一个静态的 map 集合里

好比:

/**
     * 根据不一样 ID 存储相应的缓存数据单例示例
     */
    public class SimplePreferences {

        private static Map<String, SimplePreferences> instanceMap = new ConcurrentHashMap<>();

        private SimplePreferences() {
        }

        public static SimplePreferences getInstance(String instanceId) {
            SimplePreferences instance = instanceMap.get(instanceId);
            if (instance == null) {
                synchronized (SimplePreferences.class) {
                    instance = instanceMap.get(instanceId);
                    if (instance == null) {
                        instance = new SimplePreferences();
                        instanceMap.put(instanceId, instance);
                    }
                }
            }
            return instance;
        }

        public void set(...) {
            ...
        }

        public String get(...) {
            ...
        }

    }
复制代码

其实在 Kotlin 中针对这种场景,可能使用工厂的模式会更适合,也更简单,这在后面的工厂模式的分析当中,咱们再来一块儿看一下,这里就不作多描述了。

总结

没想到码了这么多字,一个单例模式写了快五千字,有点不敢想象接下里的文章还怎么写...不过,有耐心读到这里的同窗,应该都是真爱粉,但愿可让你有所收获!

最后,简单总结回顾下:

  • 单例是一个简单并有意思的设计模式
  • 一个好的单例设计要具备延迟加载、线程安全以及效率高
  • Kotlin 中的单例实现既简单又规范
  • 单例的一些扩展知识

关于做者

相关文章
相关标签/搜索