Lazy initializaton in kotlin
Kotlin provides lazy
keyword to be used with member variables in a class. This initializes the variable only when needed. Let’s have a deeper look into lazy
in this article.
Basics
Create costly objects only when required. Load from database only when required. Load the cache only when required. These statements are guiding principle for any programmer who wants to write optimized code while doing resource intensive operations. The contrary is true as a special case when there is a need to prefetch or eager load for performance reasons.
Many use cases are about initialize once and re-use. In Java, we have final
member variables which support this pattern. Having final
member variables are so crucial in my mind that I always look for a reason why something is not final
in my code reviews. The down side is that final
variables needs to be initialized in the constructor. If the final
variable reference is costly, then the cost is front loaded. The other option is to load on-demand, but then the variable cannot be final
and becomes mutable which is undesirable in many cases. There are complex work arounds that you can employ in Java to achieve the desired results (such as using computeIfAbsent
of a ConcurrentMap
or creating your own objects that abstract the final-variable-with-create-once-on-demand paradigm)
In Kotlin, the concept of lazy
comes handy in such situations. Let’s look at a simple example:
Only when someone creates the class LazyCreator
and then access the val
mammoth
, does it get created. How does lazy
create these variables? You can read the lazy
documentation to know more. Basically there are multiple strategies supported for lazy
execution: two thread-safe ways (SYNCHRONIZED
and PUBLICATION
) and a non thread-safe way (NONE
). I won’t go deeper into that. I will provide my recommendation on when to use what:
PUBLICATION
: This strategy may result in the lazy
block to be executed multiple times. But this does not block the thread. The value from first thread to execute the block is set atomically and reused by the variable. This should preferred over SYNCHRONIZED
when the lazy
block is not costly and idempotent.
SYNCHRONIZED
: This strategy ensures that the lazy
block is executed only once. But this blocks the thread. The result is cached and reused by the variable. Prefer this if the lazy
block is a costly operation or is not idempotent or wastes resources if executed multiple times.
NONE
: This strategy invokes the lazy
block and caches the value without any locking or thread-safe strategies. Use this one with caution and only if you are sure that the variable will be accessed only by one thread.
To build a better mental model about
PUBLICATION
strategy, read about the Memoizer design pattern in Java Concurrency in Practice book. This pattern is the foundation ofcomputeIfAbstent
inConcurrentMap
API in Java.
Asynchronous lazy
So far the usages of lazy
we have seen are simple cases of resource intensive operations. But in real world, things are not as simple. Most of the resource intensive operations such as network calls or database fetch needs to happen in a background thread. In Kotlin, we make use of flows
and suspend
functions for this. lazy
cannot be used for invoking a suspend
function. We need a different strategy here.
Let’s consider a scenario where we have a costly database operation which we would like to perform exactly once, cache the result and reuse. We can use a Deferred
object to good effect.
In this example, you can see that the database operation happens exactly once and we reuse the value over and over when the public methods on AsyncLazy
is invoked. When the database values are needed, a Deferred
job is created and then reused.
The runner code can be modelled as follows which has multiple suspension points so that its truly async.
Output is as follows. Notice that multiple deferred objects were created, but only one was executed. The remaining were cancelled.
creating deferred 1
creating deferred 2
creating deferred 3
sleeping 1000 3
sleeping 1000 1
sleeping 1000 2
Trying to set deferred 3
deferred set correctly 3
Trying to set deferred 2
deferred not set, cancel 2
Trying to set deferred 1
deferred not set, cancel 1
deferred executing 3
Model(name=foo)
[Model(name=bar), Model(name=baz)]
[Model(name=foo), Model(name=bar), Model(name=baz)]
This approach is as effective as the Memoizer
design pattern in which threads could create multiple Future
objects out of which the first one successfully put into the map wins. In this example, multiple threads could end up creating Deferred
objects, but the one that is atomically set to the variable wins and gets executed.
In this article we saw how lazy
in Kotlin can be used, the various threading options and its limitations. We also learnt about a strategy for loading async scenarios lazily. Happy lazy
coding!