前言

最近在学习Kotlin,由于Google粑粑把这一语言提携为官方指定,并且对于移动端等开发得天独厚的优势,作为一名AndroidDeveloper学习Kotlin是必要的。
当初在学习Java时对单例模式的六种书写与应用记忆深刻
所以这次对不同单例模式的Java与Kotlin实现进行了分别探讨。

六种模式如下:

  • 饿汉模式
  • 懒汉模式
  • 线程安全的懒汉模式
  • 双重校验锁式
  • 静态内部类式
  • 枚举式

饿汉模式:static final field

饿汉式其实是一种比较形象的称谓。既然饿,那么在创建对象实例的时候就比较着急,饿了嘛,于是在装载类的时候就创建对象实例。
这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。

1
2
3
4
5
6
7
8
9
//Java实现
public class Singleton{
//类加载时就初始化
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}

Kotlin引入了 object类型,可以很容易声明单例模式

1
2
//Kotlin实现
object Singleton

这种方式和 Java 单例模式的饿汉式一样,不过比 Java 中的实现代码量少很多,其实是个语法糖(Kotlin漫山遍野都是语法糖)。反编译生成的 class 文件后如下:

1
2
3
4
5
6
7
8
9
public final class Singleton {
public static final Singleton INSTANCE = null;
static {
Singleton singleton = new Singleton();
}
private Singleton() {
INSTANCE = this;
}
}

从反编译的代码可以看出 object 对象实际上还是利用了 INSTANCE 静态变量,但是在Java和Kotlin混编时,Java代码中调用则需要注意,使用如下Singleton.INSTANCE.test(),Kolint中调用时只需要使用Singleton.test()
这种实现方式在类加载时就创建了单例对象,所以肯定是线程安全的,但是还是有饿汉式实现方式的问题:

  • 如果构造方法中有耗时操作的话,会导致这个类的加载比较慢。
  • 饿汉式一开始就创建实例,但是并没有调用,会造成资源浪费。
  • 还有一个 Java 饿汉式单例模式没有的问题:无法自定义构造函数,object 中不允许 constructor 函数。

懒汉模式:线程不安全

懒汉式其实是一种比较形象的称谓。既然懒,那么在创建对象实例的时候就不着急。会一直等到马上要使用对象实例的时候才会创建,懒人嘛,总是推脱不开的时候才会真正去执行工作,因此在装载对象的时候不创建对象实例。

1
2
3
4
5
6
7
8
9
10
11
//Java实现
public class Singleton{
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}

这段代码简单明了,而且使用了懒加载模式,但是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。也就是说在多线程下不能正常工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Kotlin
class Singleton private constructor(){
companion object{
private var instance : Singleton? = null
get(){
if(field == null){
field = Singleton()
}
return field
}
fun get():Singleton{
return instance!!
}
}
}

上述代码中,我们可以发现在Kotlin实现中,我们让其主构造函数私有化并自定义了其属性访问器,其余内容大同小异。

  • 如果不清楚Kotlin构造函数的使用方式。请点击 - - - 构造函数
  • 不清楚Kotlin的属性与访问器,请点击 - - -属性和字段

线程安全的懒汉模式

为了解决上面的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized)。

1
2
3
4
5
6
7
8
9
10
11
//Java实现
public class Singleton{
private static Singleton instance;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}

虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Kotlin
class Singleton private constructor() {
companion object {
private var instance: Singleton? = null
get() {
if (field == null) {
field = Singleton()
}
return field
}
@Synchronized
fun get(): Singleton{
return instance!!
}
}
}

大家都知道在使用懒汉式会出现线程安全的问题,需要使用同步锁,在Kotlin中,如果你需要将方法声明为同步,需要添加 @Synchronized 注解

双重校验锁式

双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//Java实现
public class Singleton{
/**
* volatile保证了:
* 1.instance再多线程下的并发可见性
* 2.禁止instance在操作时的指令重排序
*/
private volatile static Singleton instance = null;
public static Singleton getIntance(){
//第一次判空,保证不必要的同步
if(instance == null){
//synchronized对Singleton加全局锁,保证每次只要一个线程创建实例
synchronized(Singleton.class){
//第二次判空时为了在null的情况下创建实例
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}

有些人认为使用 volatile 的原因是可见性,也就是可以保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。

  • 从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。

但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。

相信你不会喜欢这种复杂又隐含问题的方式,当然我们有更好的实现线程安全的单例模式的办法。

1
2
3
4
5
6
7
8
//Kotlin实现
class Singleton private constructor(){
companion object{
val instance: Singleton by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED){
SingletonDemo()
}
}
}

哇!小伙伴们惊喜不,感不感动啊。我们居然几行代码就实现了多行的Java代码。其中我们运用到了Kotlin的延迟属性 Lazy

Lazy是接受一个 lambda 并返回一个 Lazy 实例的函数,返回的实例可以作为实现延迟属性的委托: 第一次调用 get() 会执行已传递给 lazy() 的 lambda 表达式并记录结果, 后续调用 get() 只是返回记录的结果。

这里还有有两个额外的知识点。

如果你了解以上知识点,我们直接来看Lazy的内部实现。

  • Lazy内部实现
1
2
3
4
5
6
public fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}

观察上述代码,因为我们传入的mode = LazyThreadSafetyMode.SYNCHRONIZED, 那么会直接走 SynchronizedLazyImpl,我们继续观察SynchronizedLazyImpl。

  • Lazy接口

SynchronizedLazyImpl实现了Lazy接口,Lazy具体接口如下:

1
2
3
4
5
6
7
public interface Lazy<out T> {
//当前实例化对象,一旦实例化后,该对象不会再改变
public val value: T
//返回true表示,已经延迟实例化过了,false 表示,没有被实例化,
//一旦方法返回true,该方法会一直返回true,且不会再继续实例化
public fun isInitialized(): Boolean
}

继续查看SynchronizedLazyImpl,具体实现如下:

  • SynchronizedLazyImpl内部实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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
// final field is required to enable safe publication of constructed instance
private val lock = lock ?: this
override val value: T
get() {
val _v1 = _value
//判断是否已经初始化过,如果初始化过直接返回,不在调用高级函数内部逻辑
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}
return synchronized(lock) {
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
}
else {
val typedValue = initializer!!()//调用高级函数获取其返回值
_value = typedValue //将返回值赋值给_value,用于下次判断时,直接返回高级函数的返回值
initializer = null
typedValue
}
}
}
//省略部分代码
}

通过上述代码,我们发现 SynchronizedLazyImpl 覆盖了Lazy接口的value属性,并且重新了其属性访问器。其具体逻辑与Java的双重检验是类似的。

到里这里其实大家还是肯定有疑问,我这里只是实例化了SynchronizedLazyImpl对象,并没有进行值的获取,它是怎么拿到高阶函数的返回值呢?。这里又涉及到了委托属性

委托属性语法是: val/var <属性名>: <类型> by <表达式>。在 by 后面的表达式是该 委托, 因为属性对应的 get()(和 set())会被委托给它的 getValue() 和 setValue() 方法。 属性的委托不必实现任何的接口,但是需要提供一个 getValue() 函数(和 setValue()——对于 var 属性)。

而Lazy.kt文件中,声明了Lazy接口的getValue扩展函数。故在最终赋值的时候会调用该方法。

1
2
3
@kotlin.internal.InlineOnly
//返回初始化的值。
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value

静态内部类式

这种方法也是《Effective Java》上所推荐的。

1
2
3
4
5
6
7
8
9
10
//Java实现
public class Singleton{
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
private Singleton(){}
public static final Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}

这种写法仍然使用JVM本身机制保证了线程安全的问题。由于静态单例对象没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化Singleton,第一次调用getInstance()时将内部类SingletonHodler,在该内部类中定义了一个static类型的变量INSTANCE,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。

由于SingletonHodler是私有的,除了getInstance()之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖JDK版本。

1
2
3
4
5
6
7
8
9
//Kotlin实现
class Singleton private constructor() {
companion object {
val instance = SingletonHolder.holder
}
private object SingletonHolder {
val holder= Singleton()
}
}

枚举式:Enum

用枚举写单例实在太简单了!这也是它最大的优点。下面这段代码就是声明枚举实例的通常做法。

1
2
3
4
//Java实现
public enum EasySingleton{
INSTANCE;
}

我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。

1
2
3
4
//Kotlin实现
enum class EasySingleton{
INSTANCE;
}