skip to content

Mutex & Semaphore

Mutex.withLock suspends instead of blocking a thread, and Semaphore.withPermit caps how many coroutines are inside a section. Interviewers ask why you should not use synchronized in a coroutine: it blocks a thread that could be running other work.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

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

open as a page

Explain what `Mutex.withLock { }` does mechanically, including how it behaves on exceptions and cancellation. How does it differ from manual `lock()`/`unlock()`?

level: middleimportance: must knowfreq 60%

basics

~10 s

withLock acquires the lock, runs your block, and always releases it afterward — even if the block throws or the coroutine is cancelled. Manual lock()/unlock() makes you remember to release it yourself.

open as a page

You need to call a flaky external API but must never have more than 5 in-flight requests at once across many coroutines. How do you implement this with coroutine sync primitives?

level: middleimportance: should knowfreq 55%

basics

~10 s

Create one shared Semaphore(5) and wrap each call in semaphore.withPermit { ... }. Coroutines that arrive when all 5 permits are taken suspend until a permit is freed.

open as a page

When would you choose a `Mutex` over alternatives like a confined single-threaded dispatcher, atomics, or `StateFlow.update`? Discuss the tradeoffs for protecting shared mutable state in coroutines.

level: seniorimportance: should knowfreq 40%

basics

~20 s

Use a Mutex when several coroutines must take turns mutating shared state and the critical section is small. Prefer atomics for simple counters, StateFlow.update for observable state, or confining work to one thread to avoid locks entirely.

open as a page

A teammate's code calls a `suspend` function while holding a `Mutex`, and that function in turn calls `withLock` on the same mutex. It hangs forever. Explain why, and how you'd fix or avoid it.

level: seniorimportance: should knowfreq 45%

basics

~20 s

Kotlin's Mutex is not reentrant. A coroutine that already holds the lock can't take it again — it suspends waiting for itself and never resumes. Fix by not re-locking: split the locked and unlocked parts.

open as a page