What causes a deadlock, and how does lock ordering prevent it?
A deadlock needs four conditions to hold at once, and lock ordering breaks exactly one of them — the trick in an interview is showing you can spot where real code violates it.
LEARN BY DOINGPut your intuition to work3 steps
What causes a deadlock, and how does lock ordering prevent it?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#The four conditions, precisely
A deadlock is a state where a set of threads are each waiting for a resource held by another thread in the same set, so none of them can ever proceed. The classic formalization (the Coffman conditions) says a deadlock requires all four of the following simultaneously:
- Mutual exclusion — a resource (a lock) can only be held by one thread at a time.
- Hold and wait — a thread holds at least one resource while waiting to acquire another.
- No preemption — a resource can only be released voluntarily by the thread holding it, never forcibly taken away.
- Circular wait — there is a cycle of threads, each waiting on a resource held by the next one in the cycle.
The reason this framing matters in an interview is that every deadlock-prevention technique works by breaking exactly one of these four conditions, not by magic. Lock ordering specifically targets circular wait, and that is worth being able to say precisely, not just "use locks in the same order."
#The classic example: transferring between two accounts
class Account(val id: Int, var balance: Double) {val lock = ReentrantLock()}// BUGGY: locks are acquired in whatever order the caller happens to pass in.fun transfer(source: Account, destination: Account, amount: Double) {source.lock.lock()try {destination.lock.lock()try {source.balance -= amountdestination.balance += amount} finally {destination.lock.unlock()}} finally {source.lock.unlock()}}
Suppose Thread 1 calls transfer(accountA, accountB, 100.0) at the same moment Thread 2 calls transfer(accountB, accountA, 50.0). Thread 1 locks A, then tries to lock B. Thread 2 locks B, then tries to lock A. Both are now waiting for a lock the other one holds, and neither will ever release the lock it has, because releasing happens in the finally block after the transfer completes — which never happens. That is all four Coffman conditions at once: mutual exclusion (each ReentrantLock allows one holder), hold-and-wait (each thread holds one lock while blocked on the other), no preemption (nothing can force either thread to give up its lock), and circular wait (A → waits on B → waits on A).
#The fix: a fixed, global lock order
Lock ordering eliminates circular wait by making every thread, regardless of which direction it is transferring money, acquire the two locks in the same global order — here, ordered by the accounts' stable, comparable IDs rather than by which account happens to be the "source" in this particular call.
// FIXED: always acquire locks in a fixed global order - by account id -// no matter which account is the source and which is the destination.fun transferOrdered(source: Account, destination: Account, amount: Double) {val first = if (source.id < destination.id) source else destinationval second = if (source.id < destination.id) destination else sourcefirst.lock.lock()try {second.lock.lock()try {source.balance -= amountdestination.balance += amount} finally {second.lock.unlock()}} finally {first.lock.unlock()}}
Now Thread 1 (transferring A → B) and Thread 2 (transferring B → A) both attempt to lock the lower-ID account first. Whichever thread gets there first locks it, and the other thread simply blocks waiting for that single lock — it never holds a different lock while doing so, so hold-and-wait cannot form a cycle. One thread proceeds fully, releases both locks, and the other then proceeds. No cycle is ever possible, because both threads always agree on the order.
#Other mitigations worth naming
Lock ordering is the most common answer, but it is not the only tool, and a strong interview answer names the tradeoffs of the alternatives too:
- Lock timeouts — using
tryLock(timeout)instead of a blockinglock(), and backing off and retrying on failure. This breaks "no preemption" in effect: a thread voluntarily gives up its held lock rather than waiting forever. The tradeoff is that you trade a deadlock for a potential livelock or retry storm if you don't add jitter to the backoff. - Reducing the scope of what you hold a lock across — never call into code you don't control (a callback, a downstream service) while holding a lock, since you have no guarantee that code won't try to reacquire a lock you already hold in a different order.
- Lock-free or atomic data structures — removing mutual exclusion for a specific piece of state entirely (e.g. a single balance modeled as an atomic value with compare-and-swap retries) sidesteps the whole problem for that data, at the cost of being considerably harder to reason about for anything beyond a single value.
- Production diagnosis — a JVM thread dump (
jstack <pid>) will often print "Found one Java-level deadlock" explicitly, naming the exact threads and locks in the cycle. That is usually faster than trying to reproduce a deadlock locally.
#Comparing the strategies
| Strategy | Which condition it breaks | Tradeoff |
|---|---|---|
| Lock ordering | circular wait | requires a stable, comparable key on every lockable resource, and every code path must respect it |
Lock timeout (tryLock) | no preemption, in effect | needs retry/backoff logic; can degrade into a livelock without jitter |
| Lock-free structures (atomics, CAS) | mutual exclusion, for that data | much harder to reason about beyond a single value |
| One coarse-grained lock | hold-and-wait, by removing concurrency entirely | kills throughput, often defeats the reason you introduced threads |
#Key takeaways
- A deadlock requires mutual exclusion, hold-and-wait, no preemption, and circular wait simultaneously; break any one and it becomes impossible.
- Lock ordering specifically eliminates circular wait by forcing every thread to request multiple locks in the same global order, regardless of the direction of the operation.
- Lock timeouts, reduced lock scope, and lock-free structures are the other real tools, each trading away something different (retry complexity, code discipline, or reasoning difficulty).
- In production, a JVM thread dump will often name a deadlock explicitly - check that before trying to reproduce it locally.