kotlin系列知识卡片 - lazy关键字

194 阅读4分钟

lazy关键字

前言
随着kotlin在Android日常开发中越来越频繁地使用,笔者觉得有必要对kotlin的一些特性做一些原理的梳理,所谓知其然知其所以然,才能更好让kotlin服务于我们的业务开发,所以会做一个kotlin系列,每个知识点简短,像个小卡片一样,便于理解和快速回顾。所有的源码为:Kotlin 标准库版本为1.9.10。

使用

  • 一般用法:使用默认的线程安全模式创建lazy对象
private val lazyObj by lazy {
    LazyObject()
}
  • 进阶用法
    • 自定义创建对象使用的锁
    // 自定义锁
    private val lock = object : Any() {}
    private val lazyObj by lazy(lock) {
        ...
    }
    
    • 自定义初始化模式
    // default,等价于 by lazy { ... }
    private val lazyObj by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { ... }
    
    private val lazyObj by lazy(LazyThreadSafetyMode.PUBLICATION) { ... }
    
    private val lazyObj by lazy(LazyThreadSafetyMode.NONE) { ... }
    

原理

默认的lazy初始化模式

通过使用属性委托,将optionalObj对象委托给lazy函数实现。lazy函数是一个分平台实现的接口,在Android开发中,我们使用的JVM的实现,文件名称为LazyJVM

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

从上可以看到默认的lazy使用的是SYNCHRONIZED模式,真正实现lazy对象创建的是SynchronizedLazyImpl函数。

lazy对象的初始化模式

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)
    }

模式说明

  • LazyThreadSafetyMode.SYNCHRONIZED

同步模式。只允许一个线程执行初始化,线程安全,高并发性能可能下降。

  • LazyThreadSafetyMode.PUBLICATION

发布模式。初始化后发布给其他线程,多线程间共享,但存在线程安全问题。

  • LazyThreadSafetyMode.NONE

非同步模式。高性能,不使用任何锁,非线程安全,多线程有线程安全问题。

SYNCHRONIZED模式源码分析

从代码实现中,我们可以知道:

  1. lazy接口实现默认为线程安全,默认使用本身作为同步锁,支持外部定制锁。
  2. 使用_value后备属性作为cache值存放的位置,且使用@Volatile修饰来保证线程间的可见性
  3. 在_value对象完成初始化后,会释放用于初始化的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
    // 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
                    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)
}

PUBLICATION模式源码分析

NONE模式较好理解,感兴趣的自行翻看源码。PUBLICATION模式我们一起来看下源码的实现:

从源码实现上,我们发现:

  • initializer_value都使用@Volatile修饰,以达成线程间可见的目的,也就是所谓的发布给其他线程的效果。对比SYNCHRONIZED模式则没有使用@Volatile修饰initializer参数。
  • 在PUBLICATION模式中使用的CAS操作来实现线程安全的操作,是一种无锁算法,所以它的性能会更优于于SYNCHRONIZED模式
  • PUBLICATION模式只保证线程安全的操作和线程间可见,所以在多线程时会存在线程安全问题:例如,当一个线程正在执行initializer函数计算Lazy属性的值时,另一个线程可能会看到_value已经被设置为非null值,但是initializer函数还没有执行完毕,导致其他线程访问到的Lazy属性的值是不完整的。
private class SafePublicationLazyImpl<out T>(initializer: () -> T) : Lazy<T>, Serializable {
    @Volatile private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // this final field is required to enable safe initialization of the constructed instance
    private val final: Any = UNINITIALIZED_VALUE

    override val value: T
        get() {
            val value = _value
            if (value !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return value as T
            }

            val initializerValue = initializer
            // if we see null in initializer here, it means that the value is already set by another thread
            if (initializerValue != null) {
                val newValue = initializerValue()
                if (valueUpdater.compareAndSet(this, UNINITIALIZED_VALUE, newValue)) {
                    initializer = null
                    return newValue
                }
            }
            @Suppress("UNCHECKED_CAST")
            return _value as T
        }

    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)

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