Fundamentals 27 min read

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.

Full-Stack Internet Architecture
Full-Stack Internet Architecture
Full-Stack Internet Architecture
Understanding and Solving Deadlocks in Java: Demonstrations, Causes, Detection Tools, and Solutions

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

DebuggingdeadlockThreadSynchronizationLock
Full-Stack Internet Architecture
Written by

Full-Stack Internet Architecture

Introducing full-stack Internet architecture technologies centered on Java

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.