24

双重检测 就比 饿汉式 高级?那 Kotlin 的 object 为什么用饿汉式?

 3 years ago
source link: http://developer.51cto.com/art/202007/621465.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一、序大家好,这里是承香墨影。

单例模式我相信大家应该不会陌生,随手抓一个程序员,让他说说最常用的 3 种设计模式,其中一定包含单例模式。

单例最重要的是,关注唯一性以及线程安全问题。而在 Java 中,单例存在多种实现范式,例如:饿汉式、懒汉式、静态内部类、双重检测等等,甚至还可以利用枚举的特性实现单例,可谓是把单例玩出了花样。

这其中,饿汉式单例实现代码是最简单的,关键代码只需一行 static final 申明对象即可,代码简单且满足需求。

但是饿汉式经常会被我们"嫌弃",日常 Review Code 时,甚至看到饿汉式单例也会「友善的建议」对方使用双重检测。

饿汉式依赖 JVM 加载类的时机,来完成静态对象的初始化,这个过程本身就是线程安全的。而它最被人诟病的,其实是无法延迟加载,完全依赖 JVM 加载类的时机,这就导致单例类加载时机不可控。也就有可能,有些资源,业务还未使用,单例类就已经准备好了,导致过多的占用了系统资源。

我们再回过头来看看 Kotlin。在 Kotlin 中,实现单例非常简单,只需要将关键字 class 替换为 object 即可。

object SomeSingleton{ fun sayHi(){}}

但 Kotlin 的 object 其实就是饿汉式单例。它难道不怕存在资源占用的问题吗?

二、Kotliin 的 object2.1 Kotlin 的 object 原理

在开始 Kotlin 的 object 选择饿汉式单例前,我们先来看看 Kotlin object 原理。

Kotlin 和 Java 可以互相调用,Kotlin 代码运行前也会被编译器编译成 Java 字节码。那我们就可以通过工具将其还原为 Java 代码进行分析。

这个转换工具, AS 原生支持。借助 AS 的 Tools → Kotlin → Show Kotlin Bytecode,就可以查看 Kotlin 编译后的 Java 字节码,再点击 Decompile 按钮,就可以将字节码转成 Java 代码。

可以看到,INSTANCE 使用 static final 声明,并且在 static 代码块内对其进行初始化,标准的饿汉式单例。

2.2 饿汉式如何保证唯一和线程安全?

前面提到,单例最重要的就是关注其唯一性和线程问题。

需要在任何情况下,都确保一个类只存在一个实例,不会因为多线程的访问,导致创建多个实例。同时也不会因为多线程而引入新的效率问题。

饿汉式单例的原理,其实是基于 JVM 的类加载机制来保证其符合单例的规范的。

简单来说,JVM 在加载类的时候,会经过初始化阶段(即 Class 被加载后,且被线程使用前)。在初始化期间,JVM 会获取一把锁,这个锁可以同步多个线程,对一个类的初始化,确保只有一个线程完成类的加载过程。这个步骤是线程安全的。

上图很清晰的描述了类的初始化锁工作流程,这里就不展开细说。

三、所谓的饿汉式问题前文提到,饿汉式单例最被人诟病的问题,在于无法实现懒加载,完全依赖虚拟机加载类的策略加载。

3.1 懒加载

懒加载的目的,说白了就是为了避免,无必要的资源浪费,在不需要的时候不加载,等什么时候业务真的需要使用到它的时候,再加载资源。

虽然饿汉式依赖虚拟机加载类的策略,但虚拟机本身也会有优化项,那就是「按需加载」的策略。

虚拟机在运行程序时,并不时在启动时,就将所有的类都加载并初始化完成,而是采用「按需加载」的策略,在真正使用时,才会进行初始化。

例如 显式的 new Class()、调用类的静态方法、反射、Class.forName() 等,这些事件首次发生时,都会触发虚拟机加载类。

例如前文中,SomeSingleton 这个单例类,我们放到一个 App 中运行一下,App 先启动,点击按钮执行 SomeSingleton.sayHi() 方法。

15:39:34.539 I/cxmyDev: App running15:39:44.606 I/cxmyDev: SomeSingleton init15:39:44.606 I/cxmyDev: SomeSingleton sayHi

注意 Log 的时间,只有点击按钮执行 SomeSingleton.sayHi() 时,该单例类才被虚拟机加载。

也就是说,通常只有在你真实使用这个类时,它才会真的被虚拟机初始化,我们并不需要担心会被提前加载而导致资源浪费。

当然,不同虚拟机的实现方式不同,这并不是强制的,但是大多数为了性能都会准守此规则。

3.2 软件设计的角度

既然饿汉式的单例,也是在首次使用时初始化,这自然就是一种类懒加载的效果。

那我们再换个角度思考,如果饿汉式单例就是在程序启动时,就初始化好了,有问题吗?

在 Java 中,其实构造一个普通对象的成本很低。那为什么到了单例模式下,就觉得是个问题呢?

主要是单例的生命周期较长,承载了业务和状态,我们不提前构造无非是 2 个问题。

单例对象本身,初始化比较复杂或耗时,提前初始化会影响其他业务;

单例初始化后,持有的资源太多,导致内存资源的浪费;

问题一:初始化逻辑复杂

如果单例在初始化阶段,存在大量的逻辑,那么也不应该等到需要使用时才初始化它,否者必然会影响到接下来的业务性能。而是应该在此之前,系统较为空闲时初始化。

例如 Android 下就可以借助 IdleHandler 在空闲时提前做一些初始化工作。

问题二:持有资源太多

系统的各项资源,从来就没有够的时候。

任何时候缓存和性能都是要平衡的,单例作为一个生命周期较长的类,更不应该长时间持有大量的资源。否者就算加载时不报错,也必然会埋下 OOM 隐患,是之后内存优化时,重点关注的对象。

在编写代码时,就思考对内存资源的合理利用,而不是等到内存问题严重时,再集中进行内存优化。合理使用弱引用优化持有资源,也是一种不错的优化手段。

另外如果初始化时,就是必须会占用一些资源,那么基于 Fail-fast 原则,有问题也应该尽早的暴露出来。

毕竟 App 崩溃在开发手里,这叫问题,而崩溃在用户手里,这就叫事故。

四、小结时刻今天我们聊了 Java 的单例,以及 Kotlin object 单例的实现原理,最后我们再小结一下。

Kotlin object 使用「饿汉式」单例,依赖 JVM 的类加载机制确保唯一和线程安全;

JVM 加载类采用「按需加载」策略,确保懒加载;

Kotlin 的 object 选择饿汉式单例,在性能和实现上都不存在问题,使用它无需顾虑。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK