Kotlin | these things you should know about Lazy

Hi, nice to meet you! 👋🏻

This article mainly shares Kotlin Lazy. I hope that after reading this article, it can help you better understand and use it.

introduction

Every student who uses kotlin will more or less use Lazy, whose Chinese translation is called delayed initialization.

The function is also relatively direct. If we have an object or field, we may only want to initialize it when it is used. At this time, we can declare it first and initialize it when it is used. By default, this initialization process is thread safe (no specific use of NONE). This benefit is the performance advantage. We don't have to initialize everything when the application or page is loaded. Compared with the past var xx = null, this method is also more convenient to a certain extent.

catalogue

  • Lazy usage
  • Analysis of Lazy internal source code design
  • Lazy usage scenario recommendation
  • How to simplify daily development

Common usage

Before we start, let's look at the simplest usage:

    private val lock = "lock"
		
  	// 1. The basic writing method (thread safety) uses Lazy's own instance internally as the lock object
    val mutableAny by lazy() {
        mutableListOf<String>()
    }

  	// 2. (thread safety) use the incoming lock as the lock object
    val mutableAnyToLock by lazy(lock) {
        mutableListOf<String>()
    }

  	// 3. The principle is the same as mode 1
    val mutableToSyn by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
        mutableListOf<String>()
    }

  	// 4. (thread safety) CAS mechanism is used internally, which is different from adding synchronization lock directly
    val mutableToPub by lazy(LazyThreadSafetyMode.PUBLICATION) {
        mutableListOf<String>()
    }

  	// 5. (thread unsafe) multiple threads may be initialized multiple times
    val mutableToNone by lazy(LazyThreadSafetyMode.NONE) {
        mutableListOf<String>()
    }

We demonstrated five ways to use it above. We may have seen or used mode 1 or mode 3 at most in our daily life, but relatively speaking, mode 4 and mode 5 are mostly used by me, mainly because they are more suitable for common scenes than others. Why will be mentioned later? I won't go into too much detail first.

Carefully observe my notes. Although there are five ways to use them, there are actually three. Why? The specific source code is shown in the following figure:

Therefore, when we analyze the source code, we mainly look at the three classes corresponding to the LazyThreadSafetyMode of the latter.

  • SynchronizedLazyImpl
  • SafePublicationLazyImpl
  • UnsafeLazyImpl

The final implementation principle is object lock, CAS and default implementation. Let's take a look along the source code.

Source code analysis

Let's first look at the most common Lazy interface:

public interface Lazy<out T> {
		// Initialized value
    public val value: T
		
		// Is it initialized
    public fun isInitialized(): Boolean
}

Lazy has three specific implementations, which we mentioned above, so let's look at their three corresponding source codes - >

SYNCHRONIZED

Synchronized lazyimpl, as follows:

internal object UNINITIALIZED_VALUE

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>,... {
    private var initializer: (() -> T)? = initializer
  	// Internally initialized value is a static class by default
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
  	// By default, Lazy's own instance is used as the lock object. If lock is not empty
    private val lock = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
          	// If it is not equal to the default value, it proves that it has been initialized and returns directly
            if (_v1 !== UNINITIALIZED_VALUE) {
                return _v1 as T
            }
						
          	// Add an object lock for initialization. The lock object is the lock passed in, and the default is the current own object
            return synchronized(lock) {
                val _v2 = _value
              	// If it is not equal to the default value, it proves that it has been initialized and returns directly
                if (_v2 !== UNINITIALIZED_VALUE) {
                   _v2 as T
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }
			...
}

Describe the process in detail with an example, such as the following code:

val mutableToSyn by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
    mutableListOf<String>()
}

When we call mutableToSyn, we actually call Lazy Value. At this time, the implementation class of Lazy is synchronized lazyimpl, so we actually call the above value implementation;

Then, in the get() method, an object lock area will be entered first, and the locked object is the lock we passed in (if it is not passed in, Lazy's own object will be used). Because the lock is added here, even if multiple threads call the get() method at the same time, there is no thread safety problem. Then get() will judge whether it has been initialized at present and return if it is. Otherwise, we will call our own callback function to initialize.

PUBLICATION

That is, SafePublicationLazyImpl. Compared with synchronized lazyimpl, there are some details that need to be paid attention to, as follows:

internal object UNINITIALIZED_VALUE

private class SafePublicationLazyImpl<out T>(initializer: () -> T) : Lazy<T>, ... {
    @Volatile private var initializer: (() -> T)? = initializer
  	// Internal value
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE

    override val value: T
        get() {
            val value = _value
            if (value !== UNINITIALIZED_VALUE) {
                return value as T
            }
          	// Save the callback function for a while
            val initializerValue = initializer
          	// If the callback function is null, it proves that the assignment has been completed
            if (initializerValue != null) {
              	// Go to get the latest value
                val newValue = initializerValue()
              	// Compare in the current object (this)_ Value, if_ value===UNINITIALIZED_VALUE, the value is assigned to newValue, and the memory address is compared
                if (valueUpdater.compareAndSet(this, UNINITIALIZED_VALUE, newValue)) {
                    initializer = null
                    return newValue
                }
            }
            return _value as T
        }

    ...

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

Describe the process in detail with an example, such as the following code:

val mutableToPub by lazy(LazyThreadSafetyMode.PUBLICATION) {
    mutableListOf<String>()
}

When we call mutableToPub, we actually call Lazy Value. At this time, the implementation class of Lazy is SafePublicationLazyImpl, so we actually call the above value implementation;

get() will judge first_ Whether value is no longer the default value. If not, it will be returned directly to prove that it has been initialized; Otherwise, judge whether the initialization function is null. If it is null, it proves that it has been initialized, and then return directly_ Value, otherwise, first call the function to get the initialized value newValue, and then use valueupdater Compareandset is updated by CAS_ Value, if the current_ Value and expected univarialized_ Set if values are equal_ Value is the new newValue, and then set the initialization function to null.

Question resolution

  • Why is initializer different from_ value to add Volatile modifier?
  • Why use atomicreferencefieldupdater Compareandset to update?

I believe many students will have such questions (if not, clap your hands for yourself) 👏🏻). If we look at the interior of synchronized lazyimpl, we will find that volatil is also modified_ value, then why?

So here are the above two questions. Let's start to dial the timeline back to the time when we learned java lock - >:

We know that each thread has its own working memory to improve efficiency. The internal operation process of the thread is mainly based on the working memory. The changes in the working memory will be refreshed to the main memory later, and the refresh time is uncertain. That is to say, in the case of multithreading, it is likely that the changes of thread A will not be known in time by thread B.

For example, there are threads A and B:

At this time, thread A needs to read the variable sum. It first obtains the variable from the main memory, and then stores it in its own working memory as A copy. In the future, all reads of thread A will directly read from its own working memory. If thread A wants to modify the variable sum at this time, it also changes the copy in working memory first, and then refreshes it to main memory. However, there is no guarantee when it will be written to main memory. When thread B reads this variable at this time, it may still get the original value, which leads to inconsistency if thread B also has self incrementing logic. This is also what we often say about visibility.

In order to solve this problem, we often adopt the following two solutions, namely synchronized or volatile.

synchronized can ensure that only one thread obtains the lock at the same time. When releasing the lock, the modification of the current variable will be actively refreshed to the main memory, so the above problems are avoided. However, in contrast, this approach requires blocking other threads. Therefore, in some scenarios, we can also use another method, such as the scenario of reading more and writing less, because if each reading is locked, it may affect our performance, and volatile can avoid this problem in this scenario.

When we modify a variable modified by volatile in the case of multithreading, it will be refreshed to main memory for the first time, and it will be visible to all threads. When other threads operate, for a variable modified by volatile, each operation needs to go to main memory to get the latest one, and then operate, which avoids the performance problems caused by blocking threads. However, it should be noted that volatile does not guarantee atomicity. It can ensure visibility and inhibit instruction reordering (by default, the compiler will optimize our code and adjust some steps. Multithreading may affect our final effect. Inhibiting reordering is to prohibit the optimization of the compiler).

What is atomicity?

Atomicity means that the operation is indivisible. Whether multi-core or single core, atomic quantities can only be operated by one thread at a time. In short, operations that will not be interrupted by the thread scheduler during the whole operation process can be considered atomic. For example, a = 1, that is, the behavior of direct assignment, which does not depend on other steps.

a + + like this does not belong to, because its steps are as follows:

  1. You need to take out the value of a first
  2. Then + 1
  3. Then write

The above three steps are connected step by step. If two threads operate at the same time, thread A executes step 1, while thread B just completes the whole step. At this time, the value of A is equivalent to the old value, and the subsequent self increment and assignment are inconsistent with our original logic.

So if we look at the above judgment logic:

If compareAndSet is not used, we will probably write such code here:

if (_valude == UNINITIALIZED_VALUE) {
       initializer = null
       _value=newVlude
       return newValue
}
  1. Compare first_ Is value the default univarialized_ VALUE
  2. If yes, set it to the new value newValue

However, the above process is obviously not an atomic operation, that is, we can't guarantee whether the assignment will be interrupted after executing the judgment logic. It is likely that other threads have assigned assignments, which is inconsistent with the expectation.

So we use AtomicReferenceFieldUpdater compareAndSet, and AtomicReferenceFieldUpdater is a method provided by jdk to update the specified object field by atomic operation. The main logic of compareAndSet method is as follows:

CAS mechanism is mainly used. Let's find the current object, that is, in memory_ Is the estimated value of value unified_ Value is the default value. If you find that the value to be actually operated is really unitialized during operation_ Value, that is, the current resource is not occupied by other threads, so we update it to newValue. Otherwise, if it is found that this value is no longer unitialized_ Value, the operation will be abandoned.

Little egg: why use atomicreferencefield updater instead of AtomicReference?

See here for details:

In the AR source code, there is also a private volatile V value in essence; The main difference between the two is that AR itself points to an object, that is, it needs to create an object more than ARFU, and the header of the object accounts for 12 bytes, and its Fields account for 4 bytes, which is 16 bytes more than ARFU. This is the case for 32-bit. If it is 64 bit, you enable - XX: + usecomponentedoops pointer compression, Header still takes up 12 bytes and Fields still takes up 4 bytes. However, if pointer compression is not enabled, header takes up 16 bytes and Fields takes up 8 bytes, taking up a total of 24 bytes, which means that each ar will create so much more memory, which will have a great impact on the pressure of GC.

NONE

UnsafeLazyImpl, as follows:

internal class UnsafeLazyImpl<out T>(initializer: () -> T) : Lazy<T>... {
    private var initializer: (() -> T)? = initializer
    private var _value: Any? = UNINITIALIZED_VALUE

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

There's nothing to say. First, judge whether value is equal to the default value. If so, call the initialization logic, otherwise return.

Because there is no thread safe processing, the calling location must be thread safe, otherwise multi-threaded calling is likely to cause multiple initialization and logic problems.

Use suggestions

After analyzing the above, it is not difficult to find that the above three have their own different scenes.

  • SYNCHRONIZED

    Thread safety. For example, a variable may be called by multiple threads at the same time, and you don't accept that the initialization function may be called multiple times, so you can use this method. However, it should be noted that because the object lock is used internally during get, it is likely to block our other threads when calling for the first time in the case of multiple threads, such as the sub thread and the main thread calling at the same time. At this time, the sub thread calls it first, The main thread will be blocked at this time. Although this time is generally very short (mainly depends on the internal logic), it still needs to be noted.

  • PUBLICATION

    Thread safe, but compared with the former, you can accept that your initialization function may be called many times, but it does not affect your final use, because only the first initialization result will be returned, which does not affect your logic. Therefore, in general, if you don't care about the above problems, we can try to write thread safe code in this way. To avoid the initialization performance loss caused by calling get to lock.

  • NONE

    When using this method for non thread safety, you should pay attention to calling under thread safety. Otherwise, multiple initialization variables may be caused under multithreading, resulting in inconsistent objects called by different threads at the beginning, resulting in logic problems. But in fact, for Android development, this is a common use. We often deal with the main thread. For example, we can use it in Activity or Fragment to lazy some fields.

Extended use

Fragment-Bundle

For a project, there is a standard key transfer, so the standardized transfer method can be used.

const val BUNDLE_KEY_TAG = "xxx_BUNDLE_KEY_TAG"

/** Add a tag to the Fragment */
fun <T : Fragment> T.argument(key: String = BUNDLE_KEY_TAG, value: Parcelable): T {
    arguments = value.toFragmentBundle(key)
    return this
}

// Fragment related
inline fun <reified T : Any> Fragment.bundles(
    key: String = BUNDLE_KEY_TAG,
) = lazy(PUBLICATION) {
    val value = arguments?.get(key) ?: throw NullPointerException("Fragment.getBundle Null?")
    if (value is T) value else throw RuntimeException("Fragment.getBundle Type mismatch")
}

When in use:

private val searchKey by bundles<SearchUserKey>()

Rv-Adapter

BaseQuickAdapter is often used in our projects. How to use lazy optimization? A simple idea is as follows:

@MainThread
fun <T> createAdapter(
    @LayoutRes layout: Int,
    obj: QuickAdapterBuilder<T>.() -> Unit
): Lazy<BaseQuickAdapter<T, BaseViewHolder>> = lazy(NONE) {
    QuickAdapterBuilder<T>().apply {
        setLayout(layout)
        obj()
    }.adapter
}
class QuickAdapterBuilder<T> {

  	@LayoutRes
    private var layout: Int = 0

    private var convert: ((holder: BaseViewHolder, data: T) -> Unit)? = null

    private var init: (BaseQuickAdapter<T, BaseViewHolder>.() -> Unit)? = null

    fun setLayout(@LayoutRes layout: Int) {
        this.layout = layout
    }

    fun onBind(convert: (holder: BaseViewHolder, data: T) -> Unit) {
        this.convert = convert
    }

    fun init(init: BaseQuickAdapter<T, BaseViewHolder>.() -> Unit) {
        this.init = init
    }

   	internal val adapter: BaseQuickAdapter<T, BaseViewHolder> =
        object : BaseQuickAdapter<T, BaseViewHolder>(layout), LoadMoreModule {
            init {
                init?.invoke(this)
            }

            override fun convert(holder: BaseViewHolder, item: T) {
                convert?.invoke(holder, item)
            }
        }
}

In summary, it is not difficult to find that by extending functions or defining top-level functions, and then just returning lazy{}, we can write elegant codes for our general business codes or components. The examples are as follows. The specific tricks depend on your own interests.

reference resources

About me

I'm Petterp, a third rate developer. If this article is helpful to you, welcome to praise and support. Your support is my greatest encouragement for continuous creation!

Tags: Android kotlin

Posted by Shawazi on Wed, 20 Apr 2022 18:55:05 +0300