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?
answer
- synchronized blocks the THREAD; Mutex suspends the COROUTINE
- coroutines share threads — blocking one starves others
- Mutex.withLock { } is suspend + finally-safe
- Mutex is NOT reentrant
- Semaphore(1) ≈ Mutex
basics
~10 ssynchronized 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 sA 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 linesimport 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 waitinggo deeper
Knows synchronized blocks the thread and Mutex suspends instead; can use withLock.
Explains thread sharing/starvation, that withLock is finally-safe, and that Mutex isn't reentrant.
Discusses dispatcher starvation/deadlock scenarios, when atomics or confinement beat a mutex, and the owner parameter.
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