skip to content

Why shouldn't you use the JVM `synchronized` block (or a `ReentrantLock`) to protect shared state inside a coroutine, and what does Kotlin offer instead?

level: juniorimportance: must knowfreq 70%

answer

  1. synchronized blocks the THREAD; Mutex suspends the COROUTINE
  2. coroutines share threads — blocking one starves others
  3. Mutex.withLock { } is suspend + finally-safe
  4. Mutex is NOT reentrant
  5. Semaphore(1) ≈ Mutex

basics

~10 s

synchronized blocks the whole thread while it waits. Coroutines share threads, so blocking one stalls others. Kotlin's Mutex instead suspends the coroutine, freeing the thread for other work.

solid answer

~40 s

A coroutine runs on a shared pool of threads. JVM `synchronized` and `ReentrantLock.lock()` are *blocking*: a coroutine waiting for the lock parks its thread, so no other coroutine can use it — that can starve or even deadlock a small dispatcher. Coroutines need a *suspending* primitive. `kotlinx.coroutines.sync.Mutex` provides `lock()`/`unlock()` and the inline `withLock { }` helper that *suspends* (not blocks) while waiting, releasing the thread back to the dispatcher. It is also not reentrant — a coroutine cannot lock a `Mutex` it already holds. For limiting *N* concurrent accessors rather than exactly one, use `Semaphore` with `withPermit { }`. Both are cooperative and integrate with cancellation.

code

kotlin · 12 lines
kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

val mutex = Mutex()
var counter = 0

suspend fun main() = coroutineScope {
    repeat(1000) {
        launch { mutex.withLock { counter++ } }
    }
} // counter == 1000, no thread blocked while waiting

go deeper

for a junior

Knows synchronized blocks the thread and Mutex suspends instead; can use withLock.

for a middle

Explains thread sharing/starvation, that withLock is finally-safe, and that Mutex isn't reentrant.

for a senior

Discusses dispatcher starvation/deadlock scenarios, when atomics or confinement beat a mutex, and the owner parameter.

for a principal

Frames the choice in terms of overall concurrency model — preferring confinement/immutability/actors over locks at scale, and reasoning about contention and fairness.

## The problem A coroutine is *not* a thread — many coroutines multiplex onto a small pool of threads (e.g. `Dispatchers.Default` ≈ number of CPU cores). A `suspend` function can pause and resume without holding its thread. JVM concurrency primitives like `synchronized(lock) { }`, `ReentrantLock.lock()`, or `Object.wait()` are **blocking**: when they wait, they *park the underlying thread*. Inside a coroutine that's harmful: - The thread is removed from the dispatcher while parked, so other ready coroutines can't run. - With a small/limited dispatcher you can **starve** or **deadlock** all available threads. - It defeats the whole point of structured, non-blocking concurrency. ## The Kotlin answer: `Mutex` `kotlinx.coroutines.sync.Mutex` is a **suspending** mutual-exclusion lock: ```kotlin import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock val mutex = Mutex() var counter = 0 suspend fun increment() { mutex.withLock { // suspends (not blocks) if held counter++ } } ``` Key traits: - `withLock { }` is an **inline** helper that calls `lock()`, runs the block, and `unlock()`s in a `finally` — exception- and cancellation-safe. - While waiting, the coroutine **suspends**; the thread returns to the dispatcher for other work. - It is **not reentrant**: locking a `Mutex` you already hold deadlocks that coroutine. ## When you need N permits: `Semaphore` `Semaphore(permits)` allows up to *N* coroutines in a region at once; `withPermit { }` acquires/releases: ```kotlin val limit = Semaphore(permits = 4) suspend fun fetch(url: String) = limit.withPermit { httpGet(url) } ``` A `Mutex` is conceptually a `Semaphore(1)` (plus owner tracking). ## Takeaway Inside coroutines, prefer **suspending** primitives (`Mutex`, `Semaphore`) over **blocking** ones (`synchronized`, `ReentrantLock`) so waiting frees the thread instead of parking it.

  • Is `Mutex` reentrant like `ReentrantLock`?
    No. If a coroutine already holding the mutex calls `lock()`/`withLock` again, it suspends forever (self-deadlock). Restructure so you don't re-enter, or pass an `owner`.
  • Could you ever still use `synchronized` correctly with coroutines?
    Only around tiny, non-suspending critical sections that you know complete instantly and never suspend — and even then `Mutex` or atomics are clearer. You must never `suspend` inside a `synchronized` block.

synchronized is standing frozen in a doorway blocking everyone behind you; Mutex is taking a number and sitting down so the room stays usable until your turn.

saying these in an interview costs you the question

  • Claiming `synchronized` and `Mutex` behave identically
  • Thinking a coroutine and a thread are the same thing
  • Saying `Mutex` is reentrant
  • Calling a `suspend` function inside a `synchronized` block
  • Not knowing `withLock` releases the lock on exception/cancellation

context