Locks, Atomics & Coordination Quiz
KOTLIN › JVM Concurrency
Which approach removes the race rather than managing it?
- Immutability, or confining all access to a single thread
- Marking every shared field @Volatile so writes publish immediately
- Storing the state in a ConcurrentHashMap
- Wrapping every access in runBlocking
Answer: Immutability, or confining all access to a single thread
If nothing can write, or only one thread ever writes, there is no race to manage. Volatile fixes visibility but not atomicity, and a concurrent map is itself a synchronisation primitive.
Two instances of a class each call the same @Synchronized instance method concurrently. What happens?
- Both proceed, since each locks on its own instance
- One blocks, because the annotation locks on the class
- Both block, because the method itself is serialised
- It throws, since the annotation is invalid on instance methods
Answer: Both proceed, since each locks on its own instance
An instance method locks on this, so two objects mean two monitors. Only a static synchronized method locks the shared Class object.
What does ReentrantLock provide that synchronized does not?
- tryLock with a timeout, interruptible acquisition, fairness, and Condition wait sets
- Reentrancy, which lets a single thread acquire the very same lock more than once over
- A happens-before edge on release
- Automatic release without a finally
Answer: tryLock with a timeout, interruptible acquisition, fairness, and Condition wait sets
Both are reentrant and both publish memory. The extras are all about controlling the wait, and the cost is that you must release it yourself.
You need at most five uploads in flight from a hundred queued. Which tool is most accurate?
- A coroutine Semaphore(5) with withPermit
- Dispatchers.IO.limitedParallelism(5)
- A Mutex held for each upload
- Dispatchers.Default, capped at core count
Answer: A coroutine Semaphore(5) with withPermit
limitedParallelism bounds concurrent execution, and an upload suspended on the network is not executing, so more than five could be outstanding. A semaphore bounds how many are inside the section. A Mutex would serialise to one.
What is the difference between a CountDownLatch and a CyclicBarrier?
- A latch is one-shot and counts events; a barrier is reusable and counts arriving threads
- A latch blocks exactly one waiting thread, whereas a barrier blocks every single participant
- A latch suits producers, a barrier suits consumers
- A latch is interruptible; a barrier is not
Answer: A latch is one-shot and counts events; a barrier is reusable and counts arriving threads
A latch counts to zero once and stays there, so waiters and counters may differ. A barrier needs every participant to arrive and wait, then resets, which suits phased computation.
Why can AtomicLong perform worse than a lock under heavy write contention?
- The compare-and-set retry loop burns CPU as threads collide repeatedly
- It acquires a hidden internal lock on every single increment operation
- It falls back to synchronized after a fixed number of attempts
- Each read forces a full memory barrier per thread
Answer: The compare-and-set retry loop burns CPU as threads collide repeatedly
CAS is optimistic and retries on collision, so contention becomes wasted spinning. LongAdder gives each thread its own cell and sums on read, trading an exact instantaneous value for throughput.
What does the ABA problem demonstrate?
- CAS checks the current value, not that the value never changed in between
- CAS can spuriously fail even when the compared value has not changed at all
- CAS is not atomic across multiple cores
- CAS requires a lock when comparing references
Answer: CAS checks the current value, not that the value never changed in between
A value that goes A to B and back to A passes the comparison even though the state moved on. AtomicStampedReference adds a version so the stale comparison fails.
Which deadlock condition does consistent lock ordering eliminate?
- Circular wait
- Mutual exclusion
- No preemption
- Hold and wait
Answer: Circular wait
A cycle requires someone acquiring in the opposite order, which a global ordering forbids. tryLock with a timeout is the alternative and attacks hold and wait instead.
What distinguishes livelock from deadlock?
- Livelocked threads are actively running and reacting to each other, making no progress
- Livelock affects only the reader threads, whereas deadlock affects only the writer threads
- Livelock resolves itself once the scheduler rebalances
- Livelock needs three or more threads
Answer: Livelocked threads are actively running and reacting to each other, making no progress
Deadlocked threads are blocked and idle in a thread dump; livelocked threads look healthy and burn CPU retrying in lockstep. Randomised backoff breaks the symmetry.
Why must a critical section that suspends use a coroutine Mutex rather than synchronized?
- A monitor is thread-owned, and a coroutine may resume on a different thread
- synchronized cannot be used inside a Kotlin suspend function at all, by design
- A Mutex is reentrant while a monitor is not
- synchronized provides no memory visibility guarantee
Answer: A monitor is thread-owned, and a coroutine may resume on a different thread
Releasing a monitor from a thread that never acquired it is illegal, and a coroutine resuming elsewhere would do exactly that. Mutex is coroutine-aware and suspends instead of blocking. Note the reentrancy is the other way round: monitors are reentrant, Mutex is not.
What happens if you try to upgrade a held read lock to a write lock on a ReentrantReadWriteLock?
- It deadlocks, because the write lock waits for all readers including yourself
- It succeeds immediately, since the very same thread already holds a lock on it
- It throws IllegalMonitorStateException immediately
- It silently downgrades the write request to a read
Answer: It deadlocks, because the write lock waits for all readers including yourself
The write lock is exclusive against every reader, and you are one of them, so you wait on yourself forever. Downgrading is legal: acquire write, then read, then release write.
What is priority inversion on Android?
- A low-priority background thread holds a lock the main thread needs, and the scheduler throttles it
- Two threads acquire the very same pair of locks in opposite orders and stall each other out
- The main thread is given a lower priority than background work
- An unfair lock lets running threads barge ahead of queued ones
Answer: A low-priority background thread holds a lock the main thread needs, and the scheduler throttles it
A background-priority thread sits in a cgroup with a small CPU share, so it is slow to release the lock and the UI stalls on work the scheduler is deliberately starving. It argues against sharing locks between main and low-priority work.
Which combination can hold a ReentrantReadWriteLock at the same moment?
- Several threads holding the read lock
- One reader plus one writer
- Two writers on different keys
- Anything, as long as all holders are reentrant
Answer: Several threads holding the read lock
Readers share: any number may hold the read lock concurrently. The write lock is exclusive against both readers and writers. That's the whole value: read-heavy structures stop serialising their readers.
A thread holding the READ lock of a ReentrantReadWriteLock tries to acquire the WRITE lock. What happens?
- It deadlocks: upgrading is not supported
- It succeeds because the lock is reentrant
- It succeeds if no other readers are active
- It throws IllegalMonitorStateException immediately
Answer: It deadlocks: upgrading is not supported
The write lock must wait for all readers to release, including the requesting thread itself, which is blocked waiting: a self-deadlock. Downgrading (acquire read while holding write, then release write) IS allowed. To "upgrade", release the read lock first and re-check state after acquiring write.
Why is a coroutine Mutex preferred over synchronized around code that calls suspend functions?
- A coroutine can resume on a different thread, breaking thread-owned monitors; Mutex suspends and is coroutine-owned
- synchronized is formally deprecated whenever the coroutines library is on the classpath, so the compiler warns you about it
- Mutex is always faster than monitor locks
- synchronized cannot appear inside a suspend function at all
Answer: A coroutine can resume on a different thread, breaking thread-owned monitors; Mutex suspends and is coroutine-owned
Monitors are held by threads. Suspend inside the block and the coroutine may resume elsewhere while the original thread still owns (or has corrupted) the monitor. mutex.withLock suspends waiters without blocking threads and releases on the resuming context correctly.
In double-checked locking for a lazy singleton, why must the INSTANCE field be @Volatile?
- So a half-constructed object can't be published to other threads
- So that the synchronized block guarding the assignment can be entered recursively
- To make the null check atomic with the assignment
- To stop two singletons being garbage collected together
Answer: So a half-constructed object can't be published to other threads
Without volatile, the reference write can be reordered before the constructor finishes, so another thread's fast-path null check sees a non-null but uninitialised object. Volatile's ordering forbids that publication reorder. (Kotlin's lazy { } or an object declaration handles this for you.)
Which tool is best for protecting a short suspend-capable read-modify-write on a shared MutableMap inside coroutines?
- synchronized(map) { ... } because it is the simplest JVM locking mechanism available
- AtomicReference with a CAS retry loop, since atomic operations never require any kind of external lock at all
- Mutex.withLock { ... } because it suspends instead of blocking and allows suspension inside the critical section
- Dispatchers.Default confinement, routing map access through the default pool
Answer: Mutex.withLock { ... } because it suspends instead of blocking and allows suspension inside the critical section
Mutex.withLock suspends rather than blocking and allows suspend calls inside the critical section. synchronized blocks the thread (bad for coroutines), AtomicReference can't protect multi-step operations on a map, and Default is a pool not a single-thread confiner.