Kotlin's Lazy Property Delegate
The Lazy Property Delegate is quite clever, despite its name
In this post, we will focus on the Kotlin Lazy Property Delegate. We’ll cover what it is, when it is useful, and how it works. We’ll also review some of the more hidden features that might not be obvious at first glance. To keep things a bit simpler, we will focus on the JVM implementations of the Lazy Property Delegate.
What is the Lazy Property Delegate?
Kotlin’s lazy
property delegate hides a lot of work from us that might otherwise be verbose or error prone. It allows us to provide a lambda representing a calculation which will only be evaluated if the property is referenced. This is great for situations where we may not end up needing the property value and the property is resource intensive or time-consuming to calculate.
For example, without the lazy delegate, whenever we instantiate an instance of SomeClass
, we pay the price for calculating theField
:
class SomeClass {
val theField: UUID = calculateTheField()
fun calculateTheField(): UUID = UUID.randomUUID().also { println("Generated -> $it") }
}
fun main() {
val s = SomeClass()
}
// Prints:
// Generated -> f452117b-9904-4cf7-9bfb-d88533317728
We haven’t even used theField
and we’ve already paid the price to generate it. In our simple case here we’re just creating a UUID
and I normally wouldn’t worry about the time to do that. Imagine we were doing something time consuming or we didn’t need theField
every time we used SomeObject
.
What if we could defer the calculation of theField
until we really needed it? Lazy Property Delegate to the rescue! Let’s modify our example so that theField
uses the Lazy Property Delegate to calculate its value:
class SomeClass {
val theField: UUID by lazy {
calculateTheField()
}
// calculateTheField() does not change
}
To use the Lazy Property Delegate, we move our calculation into a lambda, and use the by
keyword to tell Kotlin that we’re using a delegate (in this case, one called lazy
).
Now if we construct an instance of SomeClass
but do not reference theField
, we won’t incur the cost of calculating it!
// Same usage as above
fun main() {
val s = SomeClass()
}
// Does not print anything
I can think of quite a few places where this powerful tool would come in handy.
How Does It Work?
Let’s take a look at what happens when we use by lazy
. We’ll end up calling this function
from the Kotlin Standard Library:
// From LazyJVM.kt in the Kotlin Standard Library
public actual fun <T> lazy(initializer: () -> T): Lazy<T> =
SynchronizedLazyImpl(initializer)
I love that it’s valid to write actual fun
in Kotlin!
When we call that lazy
function, we specify an initializer
(our lambda function that does the actual calculation) and it is passed to a class called SynchronizedLazyIml
, which is the class that we end up delegating to. That class does all of the work to make the lazy delegate behave the way it does.
One important concern for the Lazy Property Delegate code is to determine when it needs to calculate a value. If the value has already been calculated, it is simply returned to the caller without any more work being done.
At a high level, the Lazy Property Delegate works like this:
- Have we previously calculated the value? If so, return it.
- If not, calculate the value, cache it, and return it.
“Calculate the value” seems pretty simple, right? What happens if more than one thread asks for the value and it hasn’t been cached yet? How should Kotlin handle that situation?
Dealing With Multiple Requests Before the Value Has Been Cached
I’m not going to go line-by-line and analyze SynchronizedLazyImpl
, but here is essentially what it does in pseudocode:
- If a value has already been calculated, return it
- Otherwise, synchronize on a lock object so that only one thread at a time can proceed:
- Re-check that some other thread didn’t calculate the value while we were waiting for the lock
- If not, calculate the value and store it
- In either case, return the value
The trade-off for this delegate is that if multiple callers ask for the value at the same time before it has been calculated, they will synchronize (block). However, once the value is cached no synchronization happens.
To see this in action, let’s change our calculation in order to mimic a time-consuming calculation. We’ll use Java’s Thread.sleep()
so we can also make it blocking.
fun calculateTheField(): UUID {
Thread.sleep(Random().nextInt(1000).toLong())
return UUID.randomUUID().also {
println("Generated -> $it")
}
}
With that change, our calculation takes a bit of time (somewhere between 0 and 1,000ms) before it generates a UUID
. We’ll use the also
extension to print out the value we’ve generated before it is returned so we can see what’s happening.
Next, we’ll change our main()
function to make three concurrent requests for theField
. We’ll use Kotlin Coroutines
for this:
suspend fun main() = coroutineScope {
// Create an instance of our class
val s = SomeClass()
// Make three concurrent requests for theField and wait for them all to finish
(1..3).map {
async {
println("Received -> ${s.theField}")
}
}.awaitAll()
// Print the value theField ended up with
println("Actual -> ${s.theField}")
}
Running this code gives us:
// Prints:
// Generated -> bb5c232f-a8ed-4530-9667-0052740dcd91
// Received -> bb5c232f-a8ed-4530-9667-0052740dcd91
// Received -> bb5c232f-a8ed-4530-9667-0052740dcd91
// Received -> bb5c232f-a8ed-4530-9667-0052740dcd91
// Actual -> bb5c232f-a8ed-4530-9667-0052740dcd91 <----- Same values
Our new main()
function accesses theField
three times, simultaneously. This causes the lazy delegate to synchronize their access so that it only calculates and produces one value. In our example above, one caller is busy calculating (as we can see - “Generated” is printed only once) and two are busy waiting around for that calculation to finish. Once the delegate has calculated the value, the other callers are handed the pre-calculated value. And while they did synchronize in order to wait for the value to be available, future calls for theField
will return immediately.
Now, imagine writing this ourselves every time we want a lazily-evaluated property. I’ve written this many times in Java over the years, and it’s tedious and error-prone. I’m very happy to let the Lazy Property Delegate do this work for me.
Specifying Our Own Lock
If we go digging around in the Kotlin Standard Library, we will discover another version
of the lazy
function:
// From LazyJVM.kt in the Kotlin Standard Library
public actual fun <T> lazy(lock: Any?, initializer: () -> T): Lazy<T> =
SynchronizedLazyImpl(initializer, lock)
This version lets us specify which object we lock on, in the case that multiple threads ask for our lazy field at the same time. If we don’t provide one, Kotlin uses the delegate itself to lock on, which is a sensible default.
I’ve never felt the need to specify my own lock, but we could specify one by passing it to the lazy
function:
class SomeClass {
val theField: UUID by lazy(someLockObject) {
calculateTheField()
}
}
More Options
If we dig a bit further, we find yet another version
of actual fun lazy
that takes another argument indicating what strategy the delegate should use to handle multiple concurrent requests.
I highly recommend searching through the code for the Kotlin Standard Library. It is very well written and documented, and reading high quality code is a great way to learn.
// From LazyJVM.kt in the Kotlin Standard Library
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)
}
It appears that there are three modes we can use for this: SYNCHRONIZED
(the default, which we’ve already covered), PUBLICATION
, or NONE
. Let’s examine the two new options we’ve uncovered.
Using LazyThreadSafetyMode.PUBLICATION
This mode us allows us to make a trade-off. It doesn’t synchronize like the SYNCHRONIZED
version does, but it might end up performing the calculation more than once if there is more than one thread accessing it before the value has been calculated. In the end, all of the callers will be given the same value though.
Just like above, I won’t go line-by-line to analyze SafePublicationLazyImpl
, but here’s what it is doing, in pseusocode:
- If a value has already been calculated, return it
- Make a copy of our
initializer
function calledinitializerValue
- If the
initializerValue
is not null, that means we need to calculate the value:- Calculate the value by calling
initializerValue()
- Using an atomic compare and set, check to see if some other thread has saved the value:
- If not, save the value we just calculated
- And set the
initializerValue
to null
- Calculate the value by calling
- Return the value
While it doesn’t lock, it certainly appears more complicated than the default, doesn’t it? The concern here is that while one thread is working to calculate the value, some other thread may have come along and done the work before the first one could finish. The interesting part of this is step 3.2, the atomic compare and set. We can read “atomic compare and set” to mean “Here is a value I’ve just calculated. If some specific field does not already hold a value, set it to this value I’m giving you, otherwise do nothing. And do that in one step (atomically) so no other threads can’t complicate this”.
And there’s the trade-off I mentioned above: While we avoid synchronization, we have a chance of performing more calculations than we end up needing (wasted work).
Let’s see that in action by changing how we call the Lazy Property Delegate and specify the PUBLICATION
strategy:
class SomeClass {
val theField: UUID by lazy(LazyThreadSafetyMode.PUBLICATION) {
calculateTheField()
}
}
And now we’ll run our same main()
function that makes three concurrent requests:
// Prints:
// Generated -> aa110321-b90b-419d-99eb-7d03121d339c <--- First generated value, generated
// Generated -> f051c871-c697-4910-90a4-6f96bfd09584 <--- Result ignored!
// Generated -> ce7b22ba-d3d2-466d-a5f4-58930200b672 <--- Result ignored!
// Received -> aa110321-b90b-419d-99eb-7d03121d339c <--- First generated value, returned
// Received -> aa110321-b90b-419d-99eb-7d03121d339c
// Received -> aa110321-b90b-419d-99eb-7d03121d339c
// Actual -> aa110321-b90b-419d-99eb-7d03121d339c
Well, that’s very interesting! We generated three totally different values, but all of the concurrent callers ended up with a single value in the end. As we can see, the second and third generated values are thrown away, because the “compare and set” condition did not match (some other thread had already calculated a value by the time we tried to save it).
I’ve never used this variant of the Lazy Property Delegate, but it’s nice to know the trade-off is there. It’s also nice that I don’t have to write it myself, if I want this sort of behavior.
Using LazyThreadSafetyMode.NONE
The last variant we’ll look at is called NONE
and is implemented by UnsafeLazyImpl
(what a name!).
Let’s examine what UnsafeLazyImpl
does at a high level using pseudocode:
- If we don’t already have the value, generate it.
- Return the value.
Again, we can see we’re making a trade-off: We’re not synchronizing on anything and we don’t do any work to try and make sure all concurrent callers get the same value. There is a real possibility that each concurrent caller will receive different values before the delegate has cached the value!
We probably shouldn’t use the NONE
strategy when there could be multiple concurrent callers for the value before it has been calculated. But that doesn’t mean we can’t do it anyway, just for fun.
You probably don’t want to write code to do this, we’re just trying to prove a point here.
To test this, let’s change how we call the Lazy Property Delegate once again, this time specifying the NONE
strategy:
class SomeClass {
val theField: UUID by lazy(LazyThreadSafetyMode.NONE) {
calculateTheField()
}
}
Running our main()
function yields this output:
// Prints:
// Generated -> b01cc238-6ae2-46ad-acdc-47627f25a4c6
// Received -> b01cc238-6ae2-46ad-acdc-47627f25a4c6
// Generated -> c53f8cd7-3d5f-41d2-9f4e-895291e719d0
// Received -> c53f8cd7-3d5f-41d2-9f4e-895291e719d0
// Generated -> 2f74416e-2288-4987-8603-12c5df6e56ba
// Received -> 2f74416e-2288-4987-8603-12c5df6e56ba
// Actual -> 2f74416e-2288-4987-8603-12c5df6e56ba
Yikes! We end up generating three different values (“Generated”) and each concurrent thread ends up with a different value (“Received”)! So again, don’t use this variant if there is a possibility that two or more threads will need to calculate theField
at the same time!
Usage Recommendations
Most times, it’s probably a good idea to go with the default. Kotlin’s language designers spend a lot of time considering the most common use cases and erring on the side of correctness. That means you should almost always use the Lazy Property Delegate without specifying a lock object or a LazyThreadSafetyMode
.
Having said that, here are some guidelines:
- Try to use the defaults, it’s just easier
- If you can’t stand the thought of threads waiting around for a lock, and don’t mind a little extra work being done, try
LazyThreadSafetyMode.PUBLICATION
- If you are positive only one caller will calculate the value for the first time, try
LazyThreadSafetyMode.NONE
- Really, the default is probably fine for your use case