Understanding and Solving Deadlocks in Java: Demonstrations, Causes, Detection Tools, and Solutions
This article explains Java deadlocks with synchronized and Lock examples, outlines the four necessary conditions, introduces detection tools such as jstack, jconsole, jvisualvm and jmc, and presents practical solutions including sequential locking and polling lock techniques with optimizations to avoid loops and starvation.
1. Deadlock Demonstration
Deadlock occurs when two or more execution units (threads, processes, or coroutines) wait for each other to release resources, creating a circular wait with no progress.
1.1 Deadlock using synchronized
public class DeadLockExample {
public static void main(String[] args) {
Object lockA = new Object(); // create lock A
Object lockB = new Object(); // create lock B
// thread 1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// acquire lock A first
synchronized (lockA) {
System.out.println("Thread 1: acquired lock A!");
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("Thread 1: waiting for lock B...");
synchronized (lockB) {
System.out.println("Thread 1: acquired lock B!");
}
}
}
});
t1.start();
// thread 2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// acquire lock B first
synchronized (lockB) {
System.out.println("Thread 2: acquired lock B!");
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("Thread 2: waiting for lock A...");
synchronized (lockA) {
System.out.println("Thread 2: acquired lock A!");
}
}
}
});
t2.start();
}
}The execution result shows both threads waiting for each other's lock, causing a deadlock.
1.2 Deadlock using ReentrantLock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadLockByReentrantLockExample {
public static void main(String[] args) {
Lock lockA = new ReentrantLock(); // create lock A
Lock lockB = new ReentrantLock(); // create lock B
// thread 1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
lockA.lock(); // lock A
System.out.println("Thread 1: acquired lock A!");
try {
Thread.sleep(1000);
System.out.println("Thread 1: waiting for lock B...");
lockB.lock(); // lock B
try {
System.out.println("Thread 1: acquired lock B!");
} finally {
lockB.unlock(); // release B
}
} catch (InterruptedException e) { e.printStackTrace(); }
finally {
lockA.unlock(); // release A
}
}
});
t1.start();
// thread 2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
lockB.lock(); // lock B
System.out.println("Thread 2: acquired lock B!");
try {
Thread.sleep(1000);
System.out.println("Thread 2: waiting for lock A...");
lockA.lock(); // lock A
try {
System.out.println("Thread 2: acquired lock A!");
} finally {
lockA.unlock(); // release A
}
} catch (InterruptedException e) { e.printStackTrace(); }
finally {
lockB.unlock(); // release B
}
}
});
t2.start();
}
}The result is similar: both threads hold one lock and wait for the other, forming a deadlock.
2. Causes of Deadlock
Deadlock occurs only when the following four conditions are simultaneously satisfied:
Mutual Exclusion : a resource can be held by only one execution unit at a time.
Hold and Wait : a unit holding at least one resource requests additional resources that are held by others.
No Preemption : a held resource cannot be forcibly taken away.
Circular Wait : a circular chain of units each waiting for a resource held by the next unit.
3. Deadlock Detection Tools
Four common tools can be used to analyze and locate deadlocks in Java applications:
3.1 jstack
First obtain the process ID with jps -l, then run jstack -l <PID> to generate a thread dump that includes lock information.
3.2 jconsole
Launch jconsole from the JDK bin directory, connect to the target JVM, switch to the “Threads” tab, and click “Detect deadlocks”.
3.3 jvisualvm
Open jvisualvm, select the target process, go to the “Threads” view, and use the “Thread Dump” feature to see deadlock details.
3.4 jmc (Java Mission Control)
Start jmc, open the JMX console for the target JVM, enable “Deadlock detection” in the Threads section, and view the deadlock report.
4. Deadlock Solutions
4.1 Analysis of the Four Conditions
Only the “Hold and Wait” and “Circular Wait” conditions can be broken; the other two are system properties.
4.2 Solution 1: Sequential Lock
Enforce a consistent lock acquisition order so that all threads acquire locks in the same sequence, eliminating circular wait.
public class SolveDeadLockExample {
public static void main(String[] args) {
Object lockA = new Object(); // lock A
Object lockB = new Object(); // lock B
// thread 1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockA) {
System.out.println("Thread 1: acquired lock A!");
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("Thread 1: waiting for lock B...");
synchronized (lockB) {
System.out.println("Thread 1: acquired lock B!");
}
}
}
});
t1.start();
// thread 2 (same order)
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockA) {
System.out.println("Thread 2: acquired lock A!");
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("Thread 2: waiting for lock B...");
synchronized (lockB) {
System.out.println("Thread 2: acquired lock B!");
}
}
}
});
t2.start();
}
}Running this code shows no deadlock because both threads lock A before B.
4.3 Solution 2: Polling Lock
The polling lock breaks the “Hold and Wait” condition by trying to acquire locks with tryLock(); if any lock cannot be obtained, it releases all held locks and retries.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SolveDeadLockExample {
public static void main(String[] args) {
Lock lockA = new ReentrantLock(); // lock A
Lock lockB = new ReentrantLock(); // lock B
// thread 1 using polling lock
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
pollingLock(lockA, lockB);
}
});
t1.start();
// thread 2 (normal lock order)
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
lockB.lock();
System.out.println("Thread 2: acquired lock B!");
try {
Thread.sleep(1000);
System.out.println("Thread 2: waiting for lock A...");
lockA.lock();
try { System.out.println("Thread 2: acquired lock A!"); }
finally { lockA.unlock(); }
} catch (InterruptedException e) { e.printStackTrace(); }
finally { lockB.unlock(); }
}
});
t2.start();
}
/** Polling lock */
public static void pollingLock(Lock lockA, Lock lockB) {
while (true) {
if (lockA.tryLock()) {
System.out.println("Thread 1: acquired lock A!");
try {
Thread.sleep(1000);
System.out.println("Thread 1: waiting for lock B...");
if (lockB.tryLock()) {
try { System.out.println("Thread 1: acquired lock B!"); }
finally { lockB.unlock(); System.out.println("Thread 1: released lock B."); break; }
}
} catch (InterruptedException e) { e.printStackTrace(); }
finally { lockA.unlock(); System.out.println("Thread 1: released lock A."); }
}
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
}This implementation also avoids deadlock.
4.4 Polling Lock Optimizations
4.4.1 Problem 1: Infinite Loop
If a thread holds a lock for a long time, the polling thread may loop forever trying to acquire it.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SolveDeadLockExample {
public static void main(String[] args) {
Lock lockA = new ReentrantLock();
Lock lockB = new ReentrantLock();
// thread 1 uses pollingLock (may loop forever)
Thread t1 = new Thread(() -> pollingLock(lockA, lockB));
t1.start();
// thread 2 never releases lockB (simulated bug)
Thread t2 = new Thread(() -> {
while (true) {
lockB.lock();
System.out.println("Thread 2: acquired lock B!");
try {
System.out.println("Thread 2: waiting for lock A...");
lockA.lock();
try { System.out.println("Thread 2: acquired lock A!"); }
finally { lockA.unlock(); }
} finally {
// lockB.unlock(); // omitted on purpose -> dead loop
}
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
}
});
t2.start();
}
public static void pollingLock(Lock lockA, Lock lockB) {
while (true) {
if (lockA.tryLock()) {
System.out.println("Thread 1: acquired lock A!");
try {
Thread.sleep(1000);
System.out.println("Thread 1: waiting for lock B...");
if (lockB.tryLock()) {
try { System.out.println("Thread 1: acquired lock B!"); }
finally { lockB.unlock(); System.out.println("Thread 1: released lock B."); break; }
}
} catch (InterruptedException e) { e.printStackTrace(); }
finally { lockA.unlock(); System.out.println("Thread 1: released lock A."); }
}
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
}The polling thread loops indefinitely because lockB is never released.
Improvement: Add Maximum Retry Count
Terminate the polling after a configurable number of attempts.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SolveDeadLockExample {
public static void main(String[] args) {
Lock lockA = new ReentrantLock();
Lock lockB = new ReentrantLock();
Thread t1 = new Thread(() -> pollingLock(lockA, lockB, 3));
t1.start();
// thread 2 as before (may forget to unlock)
Thread t2 = new Thread(() -> {
lockB.lock();
System.out.println("Thread 2: acquired lock B!");
try {
Thread.sleep(1000);
System.out.println("Thread 2: waiting for lock A...");
lockA.lock();
try { System.out.println("Thread 2: acquired lock A!"); }
finally { lockA.unlock(); }
} catch (InterruptedException e) { e.printStackTrace(); }
finally {
// lockB.unlock(); // omitted intentionally
}
});
t2.start();
}
public static void pollingLock(Lock lockA, Lock lockB, int maxCount) {
int count = 0;
while (true) {
if (lockA.tryLock()) {
System.out.println("Thread 1: acquired lock A!");
try {
Thread.sleep(1000);
System.out.println("Thread 1: waiting for lock B...");
if (lockB.tryLock()) {
try { System.out.println("Thread 1: acquired lock B!"); }
finally { lockB.unlock(); System.out.println("Thread 1: released lock B."); break; }
}
} catch (InterruptedException e) { e.printStackTrace(); }
finally { lockA.unlock(); System.out.println("Thread 1: released lock A."); }
}
if (count++ > maxCount) {
System.out.println("Polling lock failed after max attempts, logging or other strategy.");
return;
}
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
}4.4.2 Problem 2: Thread Starvation
When both threads poll with the same fixed interval, one thread may never acquire the lock, leading to starvation.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SolveDeadLockExample {
public static void main(String[] args) {
Lock lockA = new ReentrantLock();
Lock lockB = new ReentrantLock();
Thread t1 = new Thread(() -> pollingLock(lockA, lockB, 3));
t1.start();
Thread t2 = new Thread(() -> {
while (true) {
lockB.lock();
System.out.println("Thread 2: acquired lock B!");
try {
System.out.println("Thread 2: waiting for lock A...");
lockA.lock();
try { System.out.println("Thread 2: acquired lock A!"); }
finally { lockA.unlock(); }
} finally { lockB.unlock(); }
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
}
});
t2.start();
}
public static void pollingLock(Lock lockA, Lock lockB, int maxCount) {
int count = 0;
while (true) {
if (lockA.tryLock()) {
System.out.println("Thread 1: acquired lock A!");
try {
Thread.sleep(100);
System.out.println("Thread 1: waiting for lock B...");
if (lockB.tryLock()) {
try { System.out.println("Thread 1: acquired lock B!"); }
finally { lockB.unlock(); System.out.println("Thread 1: released lock B."); break; }
}
} catch (InterruptedException e) { e.printStackTrace(); }
finally { lockA.unlock(); System.out.println("Thread 1: released lock A."); }
}
if (count++ > maxCount) { System.out.println("Polling failed, aborting."); return; }
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
}Because both threads use the same 1‑second interval, thread 2 always acquires the lock first, causing thread 1 to starve.
Improvement: Fixed + Random Wait Time
Introduce a random component to the polling wait to break the synchronization of attempts.
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SolveDeadLockExample {
private static Random rdm = new Random();
public static void main(String[] args) {
Lock lockA = new ReentrantLock();
Lock lockB = new ReentrantLock();
Thread t1 = new Thread(() -> pollingLock(lockA, lockB, 3));
t1.start();
Thread t2 = new Thread(() -> {
while (true) {
lockB.lock();
System.out.println("Thread 2: acquired lock B!");
try {
System.out.println("Thread 2: waiting for lock A...");
lockA.lock();
try { System.out.println("Thread 2: acquired lock A!"); }
finally { lockA.unlock(); }
} finally { lockB.unlock(); }
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
}
});
t2.start();
}
public static void pollingLock(Lock lockA, Lock lockB, int maxCount) {
int count = 0;
while (true) {
if (lockA.tryLock()) {
System.out.println("Thread 1: acquired lock A!");
try {
Thread.sleep(100);
System.out.println("Thread 1: waiting for lock B...");
if (lockB.tryLock()) {
try { System.out.println("Thread 1: acquired lock B!"); }
finally { lockB.unlock(); System.out.println("Thread 1: released lock B."); break; }
}
} catch (InterruptedException e) { e.printStackTrace(); }
finally { lockA.unlock(); System.out.println("Thread 1: released lock A."); }
}
if (count++ > maxCount) { System.out.println("Polling failed, aborting."); return; }
try { Thread.sleep(300 + rdm.nextInt(8) * 100); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
}Adding a random delay prevents the two threads from colliding on the same schedule, eliminating starvation.
5. Summary
The article introduced the concept of deadlock, the four necessary conditions, detection tools (jstack, jconsole, jvisualvm, jmc), and presented two practical solutions—sequential locking and polling lock—along with optimizations to avoid infinite loops and thread starvation.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Full-Stack Internet Architecture
Introducing full-stack Internet architecture technologies centered on Java
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
