🚀 Exploring Thread-Safe Lazy Initialization with Coroutines: LazySuspend Comes into Play

Leo N
ProAndroidDev
Published in
6 min readFeb 7, 2025

🤔 Problem Statement

In October 2018, a GitHub user proposed introducing a suspending version of Kotlin’s lazy { ... } function to handle expensive initializations without blocking threads. While lazy effectively defers initialization until needed, it can still block execution, making it less suitable for coroutine-based, non-blocking applications. To solve this, contributors suggested using async(start = LAZY), allowing initialization to be deferred and executed asynchronously on first access. Several custom coroutine-based implementations emerged to bridge this gap, but despite strong interest, the feature was never integrated into the standard Kotlin library.

As of now, the Kotlin standard library does not include a built-in suspending version of the lazy function. The discussion on GitHub Issue #706 concluded without integrating this feature into the library. In the meantime, developers have explored alternative approaches, such as Mr Roman Elizarov, from his gist

📣 📣 📣 Everyone finding this gist via Google! Modern kotlinx.coroutines has out-of-the-box support for asyncLazy with the following expression: val myLazyValue = async(start = CoroutineStart.LAZY) { ... }. Use myLazyValue.await() when you need it.

as well as lazily-started-async. However, it's important to note that the use of CoroutineStart.LAZY has been debated within the community. A recent discussion in GitHub Issue #4147 considered discouraging its use due to potential complexities and difficulties in code readability.

Given these considerations, if you require suspending lazy initialization, you might opt for a custom implementation tailored to your specific use case. So, how can we implement true non-blocking lazy initialization in coroutines?

Let’s explore some practical solutions. 🚀

  • 🎯 Introduction
  • 🔥 Implementation
  • 🏆 Conclusion
  • 🌐 References

🎯 Introduction

Lazy initialization is a powerful pattern that delays object creation until it’s actually needed, improving performance and resource management. But what if you need to initialize a value asynchronously inside Kotlin coroutines? That’s where LazySuspend comes in! 🌟

🛠 Why Do We Need LazySuspend?

Kotlin provides lazy {} for synchronous lazy initialization, but it does not support suspending functions. Imagine you need to load data from a database or fetch an API response asynchronously. 🤯 Consider this example:

val storageProvider by lazy {
initializeStorageProvider() // Cannot be a suspend function 😢
}

suspend fun initializeStorageProvider(){
// ... long-running task
}

This won’t work if initializeStorageProvider is a suspend function! Instead, we need a coroutine-friendly lazy initialization mechanism. 💡

🔥 Implementation

1️⃣ Approach 1

import kotlinx.coroutines.*
import kotlin.coroutines.*

class LazySuspend<T>(private val initializer: suspend () -> T) {
@Volatile
private var cachedValue: T? = null
private val mutex = Mutex()

suspend fun getValue(): T {
if (cachedValue != null) return cachedValue!!

return mutex.withLock {
if (cachedValue == null) {
cachedValue = initializer()
}
cachedValue!!
}
}
}
  • ✅ Uses a suspending function for initialization.
  • ✅ Uses a mutex (withLock) to ensure thread safety (prevents race conditions in multithreading).
  • ✅ Stores the computed value after the first call, so subsequent calls return instantly.
suspend fun main() {
val lazyValue = LazySuspend {
println("Initializing...")
delay(1000) // Simulate long computation
"Hello, Coroutine Lazy!"
}

println("Before accessing value...")
println("Value: ${lazyValue.getValue()}") // Triggers initialization
println("Value again: ${lazyValue.getValue()}") // Uses cached value
}

// output
Before accessing value...
Initializing...
Value: Hello, Coroutine Lazy!
Value again: Hello, Coroutine Lazy!

2️⃣ Approach 2: Deferred

class LazySuspendDeferred<T>(scope: CoroutineScope, initializer: suspend () -> T) {
private val deferred = scope.async(start = CoroutineStart.LAZY) { initializer() }
suspend fun getValue(): T = deferred.await()
}

3️⃣ Approach 3: SuspendLazy from kt.academy

This function allows deferred execution of a block of code that is initialized only once in a coroutine, similar to lazy initialization. It ensures thread safety by using a Mutex and provides mechanisms to handle initialization failures and context propagation, for further details, visit the original article.

https://gist.github.com/nphausg/d5f8a5e49f199dd4903a696052f5f042

4️⃣ Approach 4: LazySuspend from ME ✌️😊

Why I choose LazySuspend instead of SuspendLazy?

Both LazySuspend and SuspendLazy are reasonable names, but the better choice depends on readability, consistency, and convention.

  • Matches the existing lazy { ... } function in Kotlin.
  • Emphasizes “lazy” behavior first, making it clear this is an alternative to lazy { ... }.
  • Easier to recognize for Kotlin developers already familiar with lazy.

The approach 3 may have some ✨ potential improvements, so that’s why I come up with LazySuspend

Avoid Unsafe Casts: The code currently casts holder to T, which might cause issues if holder was never properly assigned. Instead, you can use a sealed class or an AtomicReference.

🔐 Ensuring Thread-Safety with Mutex we ensure that only one coroutine initializes the value at a time, preventing race conditions. 🏎💨

⚠️ Handling Exceptions Gracefully: If the initializer fails, holder remains Any?, causing an unsafe cast, leading to a ClassCastException.

https://gist.github.com/nphausg/d370986b1575b7c75085a6132bc123ae
  • LazyState Sealed Class: This class is used to represent the current state of a value, whether it’s uninitialized, initialized with a value, or failed due to an exception.
  • LazySuspend Interface: This interface extends a suspending function (suspend () -> T) and adds additional properties and methods:
  • isInitialized: A boolean property to check if the value has been initialized.
  • getOrNull(): Returns the value if initialized, or null if not.
  • invoke(): The main suspending function to retrieve the lazily initialized value.
  • lazySuspend Function: This function creates an instance of LazySuspend that lazily initializes a value using the provided suspending function (initializer). It uses atomic references for thread-safety and double-checked locking to ensure that the value is initialized only once.

How can I ensure that the LazySuspend initialization runs on a background thread instead of the main thread in Kotlin?

To ensure that the lazySuspend initialization does not run on the main thread, you can explicitly use a different coroutine dispatcher when invoking the suspending function inside the initializer. You can use Dispatchers.IO, Dispatchers.Default, or any custom dispatcher to offload the work to a background thread. Here’s how you can modify your lazySuspend initialization to run on a background thread:

import kotlinx.coroutines.*

val lazyValue = lazySuspend {
withContext(Dispatchers.IO) { // Ensure this runs on a background thread
println("Initialized on thread: ${Thread.currentThread().name}")
longRunningTask() // Simulate some background work
}
}

🚨 Disclaimers

Approach 4 may have some disadvantages:

  • Complexity: The custom implementation adds complexity compared to Kotlin’s built-in lazy.
  • Potential Overhead: Double-checked locking may introduce unnecessary overhead in single-threaded scenarios.
  • Limited Use Case: The extra control may not be required for simpler lazy initialization needs.

🏆 Conclusion

The LazySuspend interface includes methods to check if the value is initialized (isInitialized), retrieve it if available (getOrNull()), and lazily initialize it when accessed (invoke()). The LazySuspend provides lazy, suspend-aware initialization while ensuring thread safety and error handling. 🚀 Whether you're fetching API data, caching results, or managing expensive computations, LazySuspend is a powerful tool in your Kotlin tools.

Give it a try in your next project! 🛠️

// Step 1: Grab from Maven central at the coordinates:

repositories {
google()
mavenCentral()
maven {
url = uri("https://maven.pkg.github.com/nphausg/loomIn")
}
}

// Step 2: Implementation from your module
$latestVersion = "0.0.1-alpha"
implementation("com.nphausg:loom:$latestVersion")

🌐 References

  • Kotlin Coroutines Documentation — Kotlinlang.org
  • Mutex in Kotlin Coroutines — Kotlin Coroutines Guide
  • Lazy Initialization in Kotlin — JetBrains Blog
  • AtomicReference in Java — Java Documentation

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Written by Leo N

🇻🇳 🇸🇬 🇲🇾 🇦🇺 🇹🇭 Engineer @ GXS Bank, Singapore | MSc 🎓 | Technical Writer . https://github.com/nphausg

Responses (1)

Write a response

Would be good to replace AtomicReference with something to make it KMP-friendly?

--