Skip to Content

Kotlin's Lazy Property Delegate

The Lazy Property Delegate is quite clever, despite its name

Posted on
Photo by Qijin Xu on Unsplash
Photo by Qijin Xu on Unsplash

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:

  1. Have we previously calculated the value? If so, return it.
  2. 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:

  1. If a value has already been calculated, return it
  2. Otherwise, synchronize on a lock object so that only one thread at a time can proceed:
    1. Re-check that some other thread didn’t calculate the value while we were waiting for the lock
    2. If not, calculate the value and store it
    3. 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:

  1. If a value has already been calculated, return it
  2. Make a copy of our initializer function called initializerValue
  3. If the initializerValue is not null, that means we need to calculate the value:
    1. Calculate the value by calling initializerValue()
    2. Using an atomic compare and set, check to see if some other thread has saved the value:
      1. If not, save the value we just calculated
      2. And set the initializerValue to null
  4. 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:

  1. If we don’t already have the value, generate it.
  2. 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:

  1. Try to use the defaults, it’s just easier
  2. 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
  3. If you are positive only one caller will calculate the value for the first time, try LazyThreadSafetyMode.NONE
  4. Really, the default is probably fine for your use case