synchronized + runBlocking Is Not Coroutine-Safe DCL
Android projects that mix `synchronized` with `runBlocking` for lazy singleton initialization risk main-thread ANRs and lose the throughput benefits of structured concurrency. Switching to `Mutex`-based DCL keeps initialization safe without blocking threads, which matters anywhere a coroutine calls into a suspend function during object creation.
A common pattern for lazy initializing a Retrofit client with a remotely fetched base URL uses `synchronized` and `runBlocking` inside a DCL check. That combination compiles and runs but is fundamentally broken in a coroutine environment. `synchronized` protects threads, not coroutines, and `runBlocking` forces an asynchronous suspend function to block the calling thread until completion.
The correct approach replaces the thread lock with a `Mutex` and makes the entire initialization path a suspend function. A `Mutex.withLock` block suspends the coroutine instead of blocking the thread, so other work can proceed while waiting for the network call or the lock itself. The outer null check still needs `@Volatile` because that read happens outside the mutex; without it, a CPU core can see a stale null even after another core has written the instance.
Each piece solves a distinct problem: `@Volatile` guarantees visibility across cores, `Mutex` provides mutual exclusion during initialization, and `suspend` keeps the wait non-blocking. Dropping any one of them turns the DCL into a latent concurrency bug that may only surface under load or on specific hardware.
Many developers treat `synchronized` as a universal concurrency primitive and assume it composes safely with coroutines, but the two models operate at different levels of abstraction — one blocks OS threads, the other suspends logical tasks.
The `runBlocking` escape hatch is often cargo-culted from test code or main-function wrappers into production paths where it silently destroys the non-blocking guarantees that coroutines are meant to provide.
Coroutine DCL is not just a syntax swap; it forces a mental model shift from 'block this thread until ready' to 'suspend until ready and let the thread do other work,' which affects how timeouts, cancellation, and error propagation are designed.
The `@Volatile` requirement in coroutine DCL reveals a subtlety that even experienced Kotlin developers miss: the fast-path read is lock-free, so it needs the same memory-visibility guarantees as any other concurrent read.
A brief correction points out that the core concept is non-blocking suspension, not just suspension in general.
Non-blocking suspension