Why is upgrading a shared (read) lock hold to exclusive (write) mode a deadlock hazard, why is downgrading the other way safe, and how do you restructure code that seems to need an upgrade?
answer
- exclusive needs zero readers; upgrader is a reader
- two upgraders deadlock on one lock
- downgrade safe: already excludes everyone
- release + reacquire + RE-CHECK
- single-holder upgradeable/intent mode
basics
~20 sIf two readers both try to upgrade, each waits for the other to release its shared hold, so neither can ever get exclusive mode — a deadlock with no cycle of distinct locks. Downgrading is safe because the holder already excludes everyone and simply relaxes. Restructure by releasing, re-acquiring exclusively, and re-validating, or by using a single-holder upgradeable mode.
solid answer
~50 sExclusive mode requires zero readers. An upgrading thread is itself a reader and will not release, so if two threads attempt the upgrade concurrently, each waits for the other's shared hold to drop — a permanent circular wait, even though only one lock object is involved. Many implementations therefore refuse upgrades outright. Downgrade — exclusive to shared — is safe and useful: the holder already excludes everyone, so relaxing to shared cannot conflict, and an atomic downgrade lets a writer publish an update and then keep reading the state it just wrote without any window in which another writer could intervene. The restructurings: (1) release the shared hold, acquire exclusive, then **re-validate**, because the state can change in the gap — the check-then-act must be repeated, not assumed; (2) use an *upgradeable* / intent mode that at most one thread may hold, compatible with readers but not with other upgraders, so the deadlock cannot form; (3) try the upgrade with a timeout and fall back to path (1).
code
text · 6 linesacquireExclusive()
newValue = compute()
state = newValue
downgradeToShared() // atomic: no writer can intervene here
try { use(state) } // guaranteed to be the value we just wrote
finally { releaseShared() }go deeper
State that two threads trying to upgrade at once wait for each other forever, and that going from write to read is fine.
Explain the mechanism via the zero-readers requirement and describe release/re-acquire/re-check.
Add the upgradeable-intent mode and its serialization cost, explain why atomic downgrade preserves the writer's own view, and stress revalidation as the correctness point.
Prefer designs that remove the question: build state off-lock and install it with a minimal exclusive section or an atomic swap, so no upgrade path exists to get wrong.
## Why the upgrade deadlocks The rule that makes shared/exclusive locks work is: exclusive mode is granted only when no shared holders remain. Now consider a thread that holds shared mode and asks for exclusive mode without releasing. ``` T1: acquireShared() ... decides an update is needed ... upgrade() T2: acquireShared() ... decides an update is needed ... upgrade() T1 waits for the reader count to reach 0 — but T2's hold keeps it at 1 T2 waits for the reader count to reach 0 — but T1's hold keeps it at 1 ``` Neither will yield, because a blocked upgrade does not release the shared hold it started from. This is a genuine deadlock, and it is easy to miss in review because the usual heuristic — 'look for two locks acquired in different orders' — does not apply. There is one lock. The cycle is between two *modes* of it. This is why many readers-writer lock implementations simply forbid upgrading, either by documenting it as unsupported, by throwing, or by deadlocking exactly as above. The safest assumption is that upgrade is not available unless the implementation explicitly documents a single-holder mechanism for it. ## Why downgrade is safe Going from exclusive to shared has no such problem. The downgrading thread already excludes every other thread, so nothing must be waited for: the lock simply changes how it accounts for the holder. Nothing another thread does can block it. Downgrading is genuinely useful, not just legal. A common shape is: take exclusive mode, compute and install a new value, then downgrade to shared and continue reading the state you just wrote. Because the downgrade is atomic — you never fully release — no other writer can slip in between the write and the subsequent reads, so the reads are guaranteed to observe your own update rather than a later one. Doing it by 'release exclusive, then acquire shared' would open exactly that window. ## Restructuring code that seems to need an upgrade The usual trigger is check-then-act: read to decide whether an update is needed, and if so, perform it. Three sound approaches: **1. Release, re-acquire, re-check.** ``` acquireShared() needed = (cache.get(k) == null) releaseShared() if needed: acquireExclusive() try: if cache.get(k) == null: // MUST re-check: the world moved cache.put(k, compute(k)) finally: releaseExclusive() ``` The re-check is the whole point. Between releasing shared and acquiring exclusive there is a real gap in which another thread may have done the same work or invalidated the entry. Code that skips the second check is the most common bug in this pattern: it duplicates work at best and overwrites a newer value at worst. Any decision made under the first read must be treated as a hint and revalidated. **2. An upgradeable (intent-exclusive) mode.** Some locks offer a third mode: compatible with shared holders, but held by at most one thread and incompatible with other upgradeable or exclusive holders. A thread that *might* write takes upgradeable mode, reads, and upgrades to exclusive if needed — safely, because by construction no second upgrader exists to deadlock against. The cost is that only one potential writer may be in that phase at a time, so the pattern serializes candidate writers even when most of them turn out not to write. **3. Timed upgrade with fallback.** If the implementation offers a timed upgrade, attempt it with a short deadline; on failure, release everything and take path 1. This avoids the permanent block but still needs the revalidation, and it needs backoff to avoid livelock between competing upgraders. ## Related design escapes If the guarded state is a small object or a map you can rebuild, sidestep the whole question: compute the new state outside any lock and install it with one very short exclusive section (or an atomic reference swap). Readers then never contend with computation, and no upgrade is ever needed. Likewise, if the expensive part is computing a value, compute it outside the lock and take exclusive mode only for the installation — holding a lock across an expensive computation is a separate defect that the upgrade discussion often hides.
- Why must the condition be re-checked after re-acquiring in exclusive mode?Between releasing shared mode and being granted exclusive mode the lock is open, so another thread may have performed the same update, performed a different one, or invalidated the state your first read observed. The result of the first read is therefore only a hint. Re-evaluating it under exclusive mode is what keeps the check-then-act atomic; skipping it duplicates work or clobbers a newer value.
saying these in an interview costs you the question
- Believes upgrading is always supported and safe
- Skips the re-check after re-acquiring in exclusive mode
- Thinks the deadlock requires two different lock objects
- Claims downgrade is equally dangerous
- Holds the lock across an expensive computation to avoid the re-check