Kotlin系列|一文看懂Lazy机制

概述

在实际开发我们经常会用到 lazy 懒加载,比如说:

Kotlin 复制代码
private val manager by lazy {
    XxxManager()
}

private val manager by lazy(lock) {
    XxxManager()
}

private val manager by lazy(LazyThreadSafetyMode.XXX) {
    XxxManager()
}

来看看 lazy 对应的实现方式:

Kotlin 复制代码
public actual fun <T> lazy(initializer: () -> T): Lazy<T> =
    SynchronizedLazyImpl(initializer)

public actual fun <T> lazy(lock: Any?, initializer: () -> T): Lazy<T> =
    SynchronizedLazyImpl(initializer, lock)

public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }

通过上面代码可以知道 lazy 实际上有三种实现方式:

  • LazyThreadSafetyMode.SYNCHRONIZED
  • LazyThreadSafetyMode.PUBLICATION
  • LazyThreadSafetyMode.NONE

Lazy 接口如下,by lazy 会委托到 value 上:

Kotlin 复制代码
public interface Lazy<out T> {
    public val value: T

    public fun isInitialized(): Boolean
}

下面分别介绍一下这三种实现方式。

SYNCHRONIZED

SynchronizedLazyImpl

Kotlin 复制代码
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
    // 锁对象,默认使用自身实例
    private val lock = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
            // 如果 value 不等于默认值,则说明已经初始化,直接返回
            if (_v1 !== UNINITIALIZED_VALUE) {
                return _v1 as T
            }
            // 初始化过程加 synchronized 锁
            return synchronized(lock) {
                val _v2 = _value
                // 再进行一次判断,已经初始化过则直接返回
                if (_v2 !== UNINITIALIZED_VALUE) {
                    _v2 as T
                } else {
                    // 初始化
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }
}

上面的注释有介绍这个流程,代码本身也很清晰。主要注意下面几个点:

  1. 当我们使用懒加载的 manager 对象时,实际上是调用了 Lazy.value,即会走上面的 get 方法。初始化过程通过 synchronized 来加锁,因此它是线程安全的。synchronized 经过多次迭代优化,已经不是当年那个重量级锁了(会经历锁升级过程),一般情况下还是比较轻量的,但在锁竞争激烈,锁持有时间长的时候(同时有多个线程使用这个 manager 实例,且初始化代码又比较耗时),会升级到重量级锁,经历用户态和内核态的切换,损耗性能。另外如果这个锁被某个子线程获取了,初始化方法又比较耗时(初始化逻辑无论在什么线程执行),此时主线程去使用这个 lazy 对象,就会陷入等待锁的过程。
  2. 上面在加锁后又判断了一次 _v2 是不是已经初始化过了,这是不是很像以前 Java 里双重检查的单例模式?原理其实也是类似的,不再赘述;另外 _valueVolatile 关键词,就如同 instanceVolatile 一样。如果有疑问可以参考 Java 单例模式
  3. 线程安全

PUBLICATION

SafePublicationLazyImpl

Kotlin 复制代码
private class SafePublicationLazyImpl<out T>(initializer: () -> T) : Lazy<T>, Serializable {
    @Volatile private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE

    override val value: T
        get() {
            val value = _value
            // 如果 value 不等于默认值,则说明已经初始化,直接返回
            if (value !== UNINITIALIZED_VALUE) {
                return value as T
            }

            val initializerValue = initializer
            if (initializerValue != null) {
                // 初始化
                val newValue = initializerValue()
                // 通过 CAS 比较 _value,如果等于 UNINITIALIZED_VALUE,则赋值为 newValue
                if (valueUpdater.compareAndSet(this, UNINITIALIZED_VALUE, newValue)) {
                    initializer = null
                    return newValue
                }
            }
            // 初始化函数为空,或者 compareAndSet 返回 false,说明已经赋值好了,直接返回
            return _value as T
        }

    companion object {
        private val valueUpdater = AtomicReferenceFieldUpdater.newUpdater(
            SafePublicationLazyImpl::class.java,
            Any::class.java,
            "_value" // fieldName
        )
    }
}

流程比较简单,可以直接看上面的注释。需要关注的点:

  1. Volatile 修饰的意义:保证可见性和有序性(禁止指令重排序),这部分不再赘述,属于老生常谈的东西了~
  2. AtomicReferenceFieldUpdater 中的 compareAndSet 方法会以原子操作去更新指定对象中的属性值,通过 CAS 方式,判断 _value 是否为 UNINITIALIZED_VALUE 值,如果是则将其更新为 newValue 并返回 true,否则不操作(说明已经被更新了),返回 false。
  3. 可以看出 initializerValue() 没有同步机制,初始化方法可能会被执行多次。
  4. 线程安全

NONE

UnsafeLazyImpl

Kotlin 复制代码
internal class UnsafeLazyImpl<out T>(initializer: () -> T) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    private var _value: Any? = UNINITIALIZED_VALUE

    override val value: T
        get() {
            if (_value === UNINITIALIZED_VALUE) {
                _value = initializer!!()
                initializer = null
            }
            return _value as T
        }
}

这种方式最简单,就是在 get 的时候判断一下是否已经初始化,是则直接返回,否则初始化再返回。

没有任何线程安全的处理,因此它是线程不安全的,多线程调用下可能会多次初始化,导致逻辑异常。

总结

通过上面的分析,我们知道了这三种方式的特点和区别,值得注意的是,我们经常直接以 by lazy {} 的方式去使用延迟加载,根据源码可以知道这种方式是 SYNCHRONIZED 的,线程安全,默认参数大部分时候也是最适合的;另外 by lazy {} 会额外创建出一个 Lazy 实例和一个 lambda 对应的 Function 实例,在某些场景需要注意(性能)


SYNCHRONIZED:

  • 线程安全
  • 整个初始化过程都被 synchronized 包围,因此多线程下初始化函数不会执行多次,但首次获取到锁的线程可能会阻塞其他线程(对于主线程也要使用这个属性的场景,需要额外注意) 。一般情况下 synchronized 也不重,可以放心使用,但在锁竞争激烈,锁持有时间长的时候,会升级到重量级锁,经历用户态和内核态的切换,损耗性能。
  • 如果没有并发操作,使用 synchronized 反而会多一点点加锁的消耗。

PUBLICATION

  • 线程安全
  • 多线程下初始化函数可能会被执行多次 ,但只有第一个初始化结果会被实际赋值,不影响使用。初始化函数不会阻塞其他线程,只有在赋值时才使用 CAS 机制
  • 这种方式虽然避免了 synchronized 同步,但可能会增加额外的工作量 (初始化函数执行多次)。实际工作中我基本上没用过这种方式,但 Kotlin 提供了这个机制,我们在某些场景可以去权衡具体该使用谁,毕竟 synchronized 有膨胀的风险。

NONE

  • 非线程安全
  • 多线程调用下可能会多次初始化,导致逻辑异常
  • 没有并发场景时,性能最好。

文中内容如有错误欢迎指出,共同进步!更新不易,觉得不错的留个再走哈~


Android视图系统:Android 视图系统相关的底层原理解析,看完定有收获。

Kotlin专栏:Kotlin 学习相关的博客,包括协程, Flow 等。

Android架构学习之路:架构不是一蹴而就的,希望我们有一天的时候,能够从自己写的代码中找到架构的成就感,而不是干几票就跑路。工作太忙,更新比较慢,大家有兴趣的话可以一起学习。

Android实战系列:记录实际开发中遇到和解决的一些问题。

Android优化系列:记录Android优化相关的文章,持续更新。

相关推荐
大白要努力!37 分钟前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟1 小时前
Android音频采集
android·音视频
小白也想学C3 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程3 小时前
初级数据结构——树
android·java·数据结构
闲暇部落5 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX7 小时前
Android 分区相关介绍
android
大白要努力!8 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee8 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood8 小时前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-11 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记