Locks, Atomics & Coordination Interview Questions
KOTLIN › JVM Concurrency
An interviewer asks how you would make a shared counter thread-safe. Walk me through your answer.
What a strong answer covers: I would start by asking what the counter is for, because the answer changes. If it is UI state, it is already a MutableStateFlow and the answer is update { it + 1 }, which is a compare-and-set loop rather than a lock and integrates with the rest of the screen. If it is a plain internal counter, AtomicInteger with incrementAndGet is the right rung: one value, one indivisible read-modify-write, no blocking, no lock to forget. I would explicitly rule out the two things people reach for by mistake. Marking it @Volatile does not work, because volatile gives visibility and ordering but never atomicity, and count++ is three operations, so increments are still lost. And synchronized works but is heavier than necessary for a single value, and brings deadlock risk if the block ever grows. Then I would qualify it, because this is where the interesting part is. Atomics are optimistic: CAS retries when another thread wins the race, so under heavy contention from many threads you get a lot of spinning and discarded work, and a lock can actually be faster. If this is a metrics counter written from many threads and read rarely, LongAdder is the right answer instead, because it keeps per-thread cells and sums them on read, trading an exact instantaneous value for far less contention. And if the counter has to change atomically alongside another field, no atomic helps, because atomicity is per value: that needs a Mutex or a synchronized block covering both.
How would you limit an app to five concurrent image uploads, and why not just use a dispatcher?
What a strong answer covers: Semaphore(5) from kotlinx.coroutines, with each upload wrapped in withPermit. I would launch all hundred uploads as coroutines, which is cheap, and let the semaphore hold back all but five. The reason to prefer that over Dispatchers.IO.limitedParallelism(5) is what each one actually bounds. limitedParallelism caps how many coroutines are executing at once, meaning how many are occupying a thread. But an upload spends nearly all its time suspended on the network, occupying no thread at all, so it does not count against that limit, and you can easily end up with far more than five requests genuinely in flight. A semaphore bounds how many coroutines are inside the section regardless of whether they are running or suspended, which is the property that matches the requirement. A Mutex would be wrong for a different reason: it is a semaphore with one permit, so it would serialise the uploads to one at a time. I would mention two refinements. The coroutine Semaphore suspends rather than blocking, so waiting coroutines cost nothing, which is what makes launching a hundred reasonable. And if the real goal is to be polite to a specific server rather than to the device, OkHttp already has a per-host dispatcher limit, so the right fix might be there rather than in application code.
You are reviewing code that holds a synchronized lock while calling a suspend function. What do you say?
What a strong answer covers: That it is broken, not merely inelegant, and the reason is that a monitor is owned by the thread that acquired it. When the coroutine suspends inside that block, it can resume on a completely different thread from the dispatcher pool, and then the exit of the synchronized block attempts to release a monitor from a thread that never acquired it. Depending on where exactly it suspends you get an IllegalMonitorStateException, or a lock that is never released and eventually deadlocks everything else that needs it. There is a second problem even when it happens to work: holding a monitor blocks the thread, so a coroutine sitting in a critical section around a network call ties up a pool thread for the entire duration, which is exactly what coroutines exist to avoid, and with the IO dispatcher capped at 64 threads you can starve the whole application. The fix is a coroutine Mutex with withLock, which is coroutine-aware: it suspends rather than blocking, so other coroutines can use the thread while one holds it, and ownership is tied to the coroutine rather than the thread so resuming elsewhere is fine. The one thing to flag when making that change is that Mutex is deliberately not reentrant, unlike a monitor. If the code inside the critical section calls back into another function that also takes the same Mutex, it will deadlock, and that is by design because reentrancy hides bugs where code accidentally depends on being called under a lock.