skip to content

questions

5

What does a mutual-exclusion lock (a mutex) actually guarantee to the code that uses it, and what does it explicitly not guarantee?

level: juniorimportance: must knowfreq 78%

answer

  1. one holder at a time, per lock instance
  2. exclusion + memory ordering, both jobs
  3. lock-to-data is a convention
  4. no fairness, no deadlock freedom, no composition
  5. owned: releaser == acquirer

basics

~20 s

At most one thread holds a given mutex, so critical sections guarded by that same lock never overlap, and a holder's writes become visible to the next holder. It promises nothing about acquisition order, fairness, deadlock freedom, or data guarded by another lock.

solid answer

~50 s

A mutex enforces **mutual exclusion**: between a successful acquire and the matching release, no other thread can be inside a section guarded by the *same* lock. It does a second job too — releasing and then acquiring the same lock orders memory, so everything the previous holder did before releasing is visible to the next holder. Exclusion plus that ordering is what makes the guarded state correct, not merely non-overlapping. What it does not give: no fairness or FIFO ordering unless the lock is explicitly fair; no protection for state that some code path touches *without* taking the lock, because the lock-to-data association is a convention nobody enforces; no deadlock freedom once two locks are involved; and no composition — two operations that are each atomic under the lock are not jointly atomic unless you hold the lock across both. Nor any performance guarantee: under contention, acquire becomes parking plus a context switch.

code

text · 9 lines
text
// NOT atomic: another thread can insert between the two calls
if (not map.containsAtomic(k)) {
    map.putAtomic(k, v)
}

// Atomic: one critical section spans the whole decision
lock.acquire()
try { if (not map.contains(k)) map.put(k, v) }
finally { lock.release() }

go deeper

for a junior

State the core rule: one thread at a time, and only against code using the same lock object.

for a middle

Add the memory-ordering half and explain why unsynchronised reads of locked data are still broken.

for a senior

Talk about what the lock does not give — fairness, deadlock freedom, composition — and about hold-time discipline: no I/O or alien calls under a lock.

for a principal

Frame the lock as a serialization budget: every critical section is a fragment of the program forced to run sequentially, so the design question is how much of the workload you are agreeing to serialize and where that ceiling lands.

## Critical sections and the exclusion rule A **critical section** is a region of code that touches shared mutable state and is correct only if no conflicting region runs at the same instant. A **mutex** is an object with two operations, *acquire* and *release*, and one invariant: at most one thread at a time sits between a successful acquire and its matching release. Everything else about locks is built on that single property. Classically, a correct mutual-exclusion protocol is judged on three properties: 1. **Mutual exclusion (safety)** — never two threads in the critical section at once. 2. **Progress / deadlock-freedom (liveness)** — if the lock is free and threads want it, some thread eventually gets in; threads not competing cannot postpone the decision forever. 3. **Bounded waiting (starvation-freedom)** — a waiting thread is overtaken only a bounded number of times. Real-world mutexes reliably give you (1) and (2). They usually do **not** give (3) unless you ask for a fair lock, because barging — letting a newly arriving thread grab a just-released lock ahead of the queue — is far faster. ## The second job: memory ordering On a multiprocessor, exclusion alone would not be enough. Compilers and CPUs reorder and buffer writes, so thread B could execute after A yet still see A's stale values. Every practical lock therefore also acts as a memory barrier: the release by A *happens before* a subsequent acquire of the *same* lock by B, so all of A's prior writes are visible to B. This is why 'I took the lock but I still saw an old value' is almost always evidence that the writer did not hold that same lock. ## The lock protects a convention, not memory Nothing in hardware or the runtime ties lock `L` to field `x`. The protection exists only because *every* piece of code that reads or writes `x` agrees to hold `L` first. Two consequences follow. Using a different lock object gives zero exclusion — the guard silently disappears. And a single unsynchronised read elsewhere reintroduces a data race even if every write is locked. Documenting the guarding lock per field is the cheap defence. ## What a mutex does not do - **No ordering fairness.** Threads may be served in any order; a hot thread can starve a cold one. - **No deadlock freedom.** Two locks acquired in opposite orders deadlock; the mutex cannot know. - **No composition.** `if (!map.contains(k)) map.put(k, v)` is not atomic just because each call is internally locked — you must hold one lock across both. - **No protection against yourself.** Blocking on I/O, or calling unknown ('alien') code while holding the lock, keeps every other thread out for the duration and invites deadlock. - **No cost-free contention.** An uncontended acquire is roughly one atomic read-modify-write on a cache line — tens of nanoseconds. A contended one parks the thread and costs a context switch, typically microseconds. ## Ownership A mutex is normally **owned**: only the thread that acquired it may release it. That ownership is what makes reentrancy, priority inheritance, and 'illegal unlock' errors definable. It is also the main semantic difference from a counting semaphore, where any thread may signal — which is why a semaphore initialised to one is a lock-shaped tool but not a mutex.

  • A field is written only while holding a lock, but one reporting path reads it without the lock. Is that safe?
    No — it is still a data race. The reader has no ordering edge with the writer, so it may observe a stale value indefinitely, or in languages with unaligned/word-tearing semantics even a partially written value. Correctness requires every access, read as well as write, to use the same lock (or an explicitly atomic read).
  • How does a mutex differ from a binary semaphore?
    A mutex has ownership: only the acquiring thread may release it, which lets the implementation support reentrancy, detect illegal unlocks, and apply priority inheritance. A binary semaphore is just a counter capped at one that any thread may signal, so it is suited to signalling between threads rather than protecting a critical section.

A mutex is the single key to a room. It stops two people entering at once, but it does not decide who gets the key next, does not stop someone climbing in the window, and does not help if a second room needs a second key.

saying these in an interview costs you the question

  • Says a lock 'makes the object thread-safe' regardless of how callers use it
  • Assumes waiters are granted the lock in FIFO arrival order
  • Forgets that reads also need the lock, not just writes
  • Thinks locking each method separately makes a sequence of calls atomic
  • Ignores the memory-visibility half of what a lock does

context

open as a page

What is a reentrant (recursive) lock, how does it differ from a non-reentrant one, and what are the arguments against reentrancy?

level: middleimportance: should knowfreq 58%

basics

~20 s

A reentrant lock lets the thread that already holds it acquire it again, tracking a hold count and only freeing the lock when the count returns to zero. A non-reentrant lock self-deadlocks on the second acquire. Reentrancy eases recursion and callbacks but lets code re-enter a critical section while invariants are broken.

open as a page

What does it mean for a lock to be fair, how does an unfair (barging) lock differ, and when is fairness worth its cost?

level: seniorimportance: should knowfreq 44%

basics

~20 s

A fair lock grants ownership in arrival order via a queue; an unfair lock lets a running thread barge in and take a just-released lock ahead of queued waiters. Barging gives much higher throughput because it avoids wake-up handoff gaps, at the price of a long starvation tail. Fairness pays when hold times are long and tail latency matters.

open as a page

What do a non-blocking lock attempt (tryLock) and a timed lock acquisition give you that a plain blocking acquire does not, and what goes wrong when they are used carelessly?

level: seniorimportance: should knowfreq 52%

basics

~20 s

They bound how long you wait: the attempt returns success or failure instead of blocking indefinitely, so you can take a fallback path, stay responsive, or release locks you already hold and retry. The risks are livelock from lockstep retries and code that ignores the failure branch.

open as a page

How do you decide how coarse or fine-grained your locking should be, what is lock striping, and what is the convoy effect that can appear under a hot lock?

level: principalimportance: should knowfreq 46%

basics

~20 s

Coarse locking is one lock for a whole structure: simple, but it serializes everything. Fine-grained locking splits protection into independent locks — striping hashes keys onto N locks — raising parallelism but adding multi-lock complexity and deadlock risk. A convoy forms when a holder is descheduled or blocks while holding, waiters pile up, and threads then advance in lockstep, collapsing throughput.

open as a page