Java Deadlock: Causes, Detection, and Prevention
java deadlock: Learn how Java deadlocks occur, how to detect them with thread dumps, and how to prevent them using lock ordering and timeouts.
A Minimal Java Deadlock Scenario
Consider two threads that each need two locks. If thread A acquires lock 1 and waits for lock 2, while thread B holds lock 2 and waits for lock 1, neither thread can proceed. That circular wait is the core of a Java deadlock.
public class DeadlockExample { private final Object lockA = new Object(); private final Object lockB = new Object(); public void methodOne() { synchronized (lockA) { System.out.println("Thread 1: holding lockA"); synchronized (lockB) { System.out.println("Thread 1: holding lockA and lockB"); } } } public void methodTwo() { synchronized (lockB) { System.out.println("Thread 2: holding lockB"); synchronized (lockA) { System.out.println("Thread 2: holding lockB and lockA"); } } } }
If one thread calls methodOne() and another calls methodTwo() at the same time, the first thread holds lockA and waits for lockB, while the second holds lockB and waits for lockA. The program hangs.
This example is deliberately simple, but the same pattern appears in real systems when locks are acquired in inconsistent order across code paths.
How Lock Ordering Creates Circular Wait
Deadlock requires four conditions: mutual exclusion, hold-and-wait, no preemption, and circular wait. In Java, synchronized blocks and ReentrantLock provide mutual exclusion. The other three conditions depend on how you write the code.
The most common cause is inconsistent lock ordering. When two threads acquire the same set of locks in a different order, a circular wait can form. In the example above, thread 1 acquires A then B, while thread 2 acquires B then A. That reversed order is the root cause.
Detecting Deadlocks with Thread Dumps
When an application hangs, the first step is to capture a thread dump. On a Unix-like system, send SIGQUIT with kill -3 <pid>, or use jstack <pid>. The dump shows every thread's stack trace and which monitors it holds or is waiting for.
In the deadlock example, the dump contains two threads with a "Found one Java-level deadlock" message. The JVM detects the cycle and lists the threads, the locks they hold, and the locks they are waiting for. That output tells you exactly which code paths are involved.
For a running application, you can also trigger a thread dump programmatically using ThreadMXBean:
ThreadMXBean tmx = ManagementFactory.getThreadMXBean(); long[] ids = tmx.findDeadlockedThreads(); if (ids != null) { ThreadInfo[] infos = tmx.getThreadInfo(ids, true, true); for (ThreadInfo info : infos) { System.out.println(info.getThreadName() + " is deadlocked"); } }
findDeadlockedThreads() returns the IDs of threads that are in a deadlock cycle, or null if none exist. This is useful for automated detection in a health check.
Preventing Deadlocks with Consistent Lock Ordering
The simplest prevention is to acquire locks in a global, consistent order across all threads. If every code path acquires lockA before lockB, the circular wait cannot occur because no thread will hold lockB while waiting for lockA.
public void methodOne() { synchronized (lockA) { synchronized (lockB) { // safe } } } public void methodTwo() { synchronized (lockA) { synchronized (lockB) { // safe } } }
This rule works when the lock set is known in advance and the ordering is documented. For more complex lock sets, assign a numeric order to each lock and acquire them in ascending order.
Using tryLock with a Timeout
When you cannot guarantee lock ordering, ReentrantLock offers a fallback: tryLock with a timeout. Instead of blocking indefinitely, a thread gives up after a configured wait and can retry or release locks it already holds.
ReentrantLock lockA = new ReentrantLock(); ReentrantLock lockB = new ReentrantLock(); boolean gotA = lockA.tryLock(100, TimeUnit.MILLISECONDS); if (gotA) { try { boolean gotB = lockB.tryLock(100, TimeUnit.MILLISECONDS); if (gotB) { try { // critical section } finally { lockB.unlock(); } } else { // release lockA and retry or fail } } finally { lockA.unlock(); } }
tryLock does not prevent deadlock by itself; it breaks the hold-and-wait condition when a thread releases its acquired locks after failing to get the next one. The timeout must be chosen carefully: too short causes unnecessary retries, too long delays failure detection.
Runtime Deadlock Detection and Monitoring
For production systems, relying on thread dumps after a hang is reactive. A better approach is to run a periodic check using ThreadMXBean.findDeadlockedThreads() and log the stack traces when a cycle is found. This gives you a clear signal before users report a stalled request.
Keep in mind that findDeadlockedThreads() only detects cycles that are already formed. It does not prevent them. Use it as an observability tool, not a fix.
Production Impact and Performance Considerations
Deadlocks are not just a correctness issue; they degrade throughput and availability. A single deadlocked thread can block a shared resource, causing other threads to queue behind it. In a web application, that can turn into a thread pool exhaustion and a complete service outage.
Prevention via lock ordering has minimal runtime cost because it only changes the order of acquisition. Using tryLock adds a small overhead from the timeout check and the retry logic, but it is usually negligible compared to the cost of a hang. The real cost is complexity: you must decide what to do when a lock cannot be acquired, and that decision often requires a retry policy or a fallback path.
When designing concurrent code, prefer the smallest critical section possible. Holding a lock for less time reduces the window in which another thread can wait for it. Combine that with consistent lock ordering and you eliminate the most common deadlock scenarios without adding significant overhead.