Kotlin 的 Lazy , 能玩出什么花样?
本文主要分享 Kotlin Lazy 相关,希望看完本篇,可以帮助到你更好的理解与使用。
作用也相对直接,如果我们有某个对象或字段,我们可能只想使用时再初始化,此时就可以先声明,等到使用时再去初始化,并且这个初始化过程默认也是线程安全(不特定使用NONE)。这样的好处就是性能优势,我们不必应用或者页面加载时就初始化一切,相比过往的 var xx = null ,这种方式一定程度上也更加便捷。
Lazy 使用方式 Lazy 内部源码设计解析 Lazy 使用场景推荐 如何简化日常开发
SynchronizedLazyImpl SafePublicationLazyImpl UnsafeLazyImpl
最终的实现原理上也即 对象锁 ,CAS , 默认实现 三种方式,我们接下来顺着源码一起来看看吧。
SYNCHRONIZED
PUBLICATION
疑问解析
为什么 initializer 与 _value 要增加 Volatile 修饰? 为什么要使用 AtomicReferenceFieldUpdater.compareAndSet 去更新 ?
线程A此时要读取变量sum,其首先去主存中获取该变量,然后存到自己的工作内存里作为一个副本,以后线程A的所有读取都会直接读取自己工作内存里的。如果此时线程A要修改变量sum,同样也是先对工作内存中的副本做更改,然后再刷新到主存里,但至于什么时候写入主存中,无法保证。而线程B 此时去读取这个变量,可能获取的 还是原来的值 ,这就导致了如果线程B也有自增逻辑,那么就会导致不一致。这也是我们常说的 可见性 问题。
原子性就是指该操作是不可再分的。不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。比如 a = 1,即直接赋值的这种行为,其不依赖其他步骤。 而像这种 a++ 的就不属于,因为其步骤如下:
需要先取出a的值 然后再进行+1 然后再写入 上述三个步骤步步相连,如果此时两个线程同时操作,A线程在执行步骤1后,而B线程此时刚好完成了整个步骤,此时A的值就相当于旧值,那么接下来的自增与赋值就已经与我们原有的逻辑不一致。
compareAndSet()
先比较 _value 是不是默认值 UNINITIALIZED_VALUE 是就设置为新值 newValue
主要是利用了CAS机制。我们去查找当前对象,也就是内存中 _value 的预估值是否为 UNINITIALIZED_VALUE 默认值,如果操作时,发现 这个要实际操作的值真的是 UNINITIALIZED_VALUE ,也就是当前资源并没有其他线程占用 ,那么我们就将其更新为 newValue 。否则如果发现这个值已经不是 UNINITIALIZED_VALUE , 则 放弃此次操作 。
具体见此: AR源码里面,本质也有一个private volatile V value; 存在,这两者的差异点主要在于AR本身是要指向一个对象的,也就是要比ARFU多创建一个对象,而这个对象的头(Header)占12个字节,它的成员(Fields)占4个字节,也就比ARFU要多出来16个字节,这是对于32位的是这种情况,如果是64位的话,你启用了-XX:+UseComparessedOops 指针压缩的话,那么Header还是占用12个字节,Fields也还是占用4个字节,但如果没有启用指针压缩的话,那么Header是占16个字节,Fields占用8个字节,总共占用24个字节,那么就说明每创建一个AR都会多出来这么多的内存,那么对GC的压力就有很大的影响了。
NONE
因为没有做任何线程安全的处理,所以必须其调用位置必须是线程安全,否则多线程下调用很可能会造成多次初始化导致逻辑问题。
SYNCHRONIZED 线程安全 比如有某个变量,可能会被多个线程同时调用,而且你不接受初始化函数可能会调用多次,所以可以使用此方法,但需要注意的是,因为get时其内部使用了对象锁,所以在多线程情况下 第一次 调用时,很可能会阻塞我们的其他线程,比如子线程和主线程同时调用,此时子线程先调用到,那主线程此时就会被阻塞,虽然这个时机其实一般而言很短(主要取决于内部逻辑),但也仍需要注意。 PUBLICATION 线程安全 但是相比前者,你可以接受 你的初始化函数可能被调用多次 ,但并不影响你最终的使用,因为只有第一个初始化结果的才会被返回,并不影响你的逻辑,所以一般情况下,如果不在意上述问题,我们可以尽量采用这种方式去编写线程安全代码。以避免调用get加锁导致初始化性能损耗。 NONE 非线程安全 使用此方式,需要注意在线程安全的情况下调用,否则多线程下很可能造成多个初始化变量,导致不同的线程初始时调用的对象甚至不一致,造成逻辑问题。但其实对于 Android 开发而言,这种反而是比较常见的用途,与我们打交道的往往都是主线程,比如我们可以用在Activity 或者 Fragment 中去 lazy 一些字段等。
Fragment-Bundle
private val searchKey by bundles<SearchUserKey>()
Rv-Adapter
参考
Kotlin Lazy — What are they? How to use them?
https://medium.com/@kiitvishal89/kotlin-lazy-know-them-inside-out-94ab4de1676d
为什么volatile不能保证原子性而Atomic可以?
https://www.cnblogs.com/Mainz/p/3556430.html
volatile为什么不能保证原子性?
https://www.zhihu.com/question/329746124
AtomicReference和AtomicReferenceFieldUpdater有何异同?
https://apkok.github.io/2021/04/26/%E9%9D%A2%E8%AF%95%E9%A2%98%E5%8D%81%EF%BC%9AAtomicReference%E5%92%8CAtomicReferenceFieldUpdater%E6%9C%89%E4%BD%95%E5%BC%82%E5%90%8C%EF%BC%9F/