概述
在实际开发我们经常会用到 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
}
}
}
}
上面的注释有介绍这个流程,代码本身也很清晰。主要注意下面几个点:
- 当我们使用懒加载的
manager
对象时,实际上是调用了Lazy.value
,即会走上面的get
方法。初始化过程通过synchronized
来加锁,因此它是线程安全
的。synchronized
经过多次迭代优化,已经不是当年那个重量级锁了(会经历锁升级过程),一般情况下还是比较轻量的,但在锁竞争激烈,锁持有时间长的时候(同时有多个线程使用这个 manager 实例,且初始化代码又比较耗时),会升级到重量级锁,经历用户态和内核态的切换,损耗性能。另外如果这个锁被某个子线程获取了,初始化方法又比较耗时(初始化逻辑无论在什么线程执行),此时主线程去使用这个 lazy 对象,就会陷入等待锁的过程。 - 上面在加锁后又判断了一次
_v2
是不是已经初始化过了,这是不是很像以前 Java 里双重检查的单例模式?原理其实也是类似的,不再赘述;另外_value
加Volatile
关键词,就如同instance
加Volatile
一样。如果有疑问可以参考 Java 单例模式。 - 线程安全
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
)
}
}
流程比较简单,可以直接看上面的注释。需要关注的点:
Volatile
修饰的意义:保证可见性和有序性(禁止指令重排序),这部分不再赘述,属于老生常谈的东西了~AtomicReferenceFieldUpdater
中的compareAndSet
方法会以原子操作去更新指定对象中的属性值,通过 CAS 方式,判断_value
是否为UNINITIALIZED_VALUE
值,如果是则将其更新为 newValue 并返回 true,否则不操作(说明已经被更新了),返回 false。- 可以看出
initializerValue()
没有同步机制,初始化方法可能会被执行多次。 - 线程安全
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优化相关的文章,持续更新。