How to Avoid Deadlock in Java — Lock Ordering, tryLock, and Design Techniques
Fast recall
Quick revision
Core answer
ReentrantLock.tryLock(1, SECONDS) instead of unconditional lock(); if it fails, release everything held and retry. (3) Open calls — never call external methods (callbacks, listeners, other services) while holding a lock; release first. (4) Reduce lock scope — minimize the code inside synchronized blocks; narrow the critical section. (5) Lock-free alternatives — ConcurrentHashMap, AtomicReference, immutable data + atomic reference swap. (6) Single global lock — simple, no cycles possible; fine for low-contention scenarios. Bonus: prefer `ReentrantLock` over `synchronized` on Java 21 (no virtual-thread pinning, plus tryLock support). Actively monitor with ThreadMXBean.findDeadlockedThreads(). Most production deadlocks come from inconsistent lock ordering — enforce it via code review and static analysis.Whether the candidate knows multiple prevention techniques (not just one), can discuss trade-offs, and understands that lock ordering is the cornerstone
Only mentioning one technique. Assuming synchronized is always safe. Not recognizing that calling external methods while holding a lock is a common deadlock source
Give concrete ordering strategies (by identityHashCode, by documented precedence), mention the 'open calls' principle from Java Concurrency in Practice, cite tryLock patterns with backoff, and discuss lock-free alternatives as the ultimate avoidance
Interview-ready guide
Interview answer
Deadlock requires all four Coffman conditions simultaneously — break any one and you're safe.
1. Lock ordering — the cornerstone
- Always acquire multiple locks in the same global order
- Order by identity hash:
System.identityHashCode(obj)— smaller first - Or by domain ID:
account.id— lower ID first - Or by documented precedence: "User before Order before Inventory"
- Zero runtime cost; deadlock impossible by construction
- Handle rare hash collisions with a tie-breaker lock
2. tryLock with timeout and back-off
ReentrantLock.tryLock(1, SECONDS)— returns false if unavailable- If can't get both locks, release what you have and retry
- No thread ever blocked while holding another lock
- Risk: livelock if two threads keep retrying in lockstep — add randomized
Thread.sleepback-off tryLock(timeout)with a finite timeout lets you fail fast and return an error
3. Open calls
- Never call external methods while holding a lock
- Listeners, callbacks, other services — all unknown code
- They might acquire other locks or call back into you
- Pattern: copy collections under lock, iterate without:
snapshot = copyOf(listeners); // release lock for (l : snapshot) l.onUpdate(); - Straight from Brian Goetz's Java Concurrency in Practice
4. Reduce lock scope
- Smaller critical sections = fewer opportunities for ordering bugs
- Never include I/O, heavy CPU, external calls, or
sleepinside a lock - Use pattern: compute outside, update inside
5. Lock-free alternatives
ConcurrentHashMapwithcomputeIfAbsent— atomic, lock-free for typical opsAtomicReferencewithupdateAndGet— CAS-based- Immutable data +
volatilereference swap — readers never lock CopyOnWriteArrayListfor read-heavy structures- Ultimate avoidance: no locks, no deadlock possible
6. Single global lock
- One lock for the whole system
- No cycles possible (only one lock)
- Fine for simple, low-contention systems
- Doesn't scale but gives correctness
Java 21 specific — prefer `ReentrantLock`
synchronizedpins virtual threads to carriers- Under load: carrier starvation
ReentrantLockuses Loom-awareLockSupport.park— unmounts cleanly- Also has
tryLock,Condition, fairness options
Monitoring in production
ManagementFactory.getThreadMXBean().findDeadlockedThreads()on a 30-60s schedule- Alert on-call when it fires
- Capture thread dump automatically
Common deadlock sources to audit
- Transfers between two objects each with their own lock
- Nested services holding locks during callbacks
- ConcurrentHashMap.computeIfAbsent lambda that acquires another lock (bucket lock + extra lock = potential cycle)
- Static initializers with cross-class dependencies
- Shutdown sequences holding locks across pool termination
Recommendation: Adopt lock ordering as the default for any code that acquires multiple locks — order by identity hash or domain key, document the rule, enforce in code review. Layer in `tryLock(timeout)` for graceful degradation under extreme contention. Apply the open-calls principle ruthlessly: copy collections under lock, iterate without; never hold a lock across a listener callback or external call. For most shared state, reach for lock-free alternatives first — ConcurrentHashMap, AtomicReference, immutable-plus-swap; they sidestep the whole class of problems. On Java 21+, default to ReentrantLock over synchronized. And run `ThreadMXBean.findDeadlockedThreads()` as a scheduled health check so the first time a deadlock ever forms in production, you get paged instead of learning about it from customer complaints.
How to Avoid Deadlock in Java — Lock Ordering, tryLock, and Design Techniques
Direct Answer
Six proven techniques, roughly in order of effectiveness: (1) Lock ordering — always acquire locks in the same global order (e.g., by lock identity hash or by a documented precedence). Eliminates cyclic waits by construction. (2) tryLock(timeout) — use ReentrantLock.tryLock(1, SECONDS) instead of unconditional lock(); if it fails, release everything held and retry. (3) Open calls — never call external methods (callbacks, listeners, other services) while holding a lock; release first. (4) Reduce lock scope — minimize the code inside synchronized blocks; narrow the critical section. (5) Lock-free alternatives — ConcurrentHashMap, AtomicReference, immutable data + atomic reference swap. (6) Single global lock — simple, no cycles possible; fine for low-contention scenarios. Bonus: prefer ReentrantLock over synchronized on Java 21 (no virtual-thread pinning, plus tryLock support). Actively monitor with ThreadMXBean.findDeadlockedThreads(). Most production deadlocks come from inconsistent lock ordering — enforce it via code review and static analysis.
Common Mistakes & Pitfalls
Only mentioning one technique. Assuming synchronized is always safe. Not recognizing that calling external methods while holding a lock is a common deadlock source
Check your understanding
Answer these without looking back at the article.
- How does lock ordering eliminate deadlock? Why is the cycle impossible?
- What's the difference between deadlock and livelock, and how do you prevent each?
- Why is 'open calls' important? Show a concrete example.
- On Java 21, why should you prefer `ReentrantLock` over `synchronized`?
- How would you handle a hash collision in identity-hash-based lock ordering?
Related interview prep categories
2Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.