View All
View All
View All
View All
View All
View All
View All
View All
View All
View All
View All
View All
View All
View All
View All
View All
View All
View All
View All
View All
View All
View All
View All
View All
View All
View All
View All

Deadlock in Java: Understanding, Examples, and Prevention Techniques

Updated on 29/04/20254,573 Views

Deadlock in Java Programming occurs when two or more threads are blocked forever, each waiting for a resource that another thread holds. This critical concurrency issue can completely halt your application's execution with no automatic recovery mechanism. Understanding deadlock in Java multithreading is essential for developing concurrent applications. In this guide, we'll examine what causes deadlocks, explore deadlock examples in Java, and discuss effective strategies for how to avoid deadlock in Java.

Step up your Java skills! Join upGrad’s software engineering course for hands-on learning and professional guidance.

What is a Deadlock in Java?

A deadlock happens when multiple threads are unable to proceed because each is waiting for resources held by another thread in the same waiting group. This creates a circular dependency where no thread can continue execution, resulting in a permanently blocked state.

Also read: Thread in Java

For a deadlock to occur, the following four conditions (known as Coffman conditions) must be present simultaneously:

  1. Mutual Exclusion: At least one resource must be held in a non-sharable mode, meaning only one thread can use it at a time.
  2. Hold and Wait: A thread must be holding at least one resource while waiting to acquire additional resources held by other threads.
  3. No Preemption: Resources cannot be forcibly taken away from a thread; they must be released voluntarily.
  4. Circular Wait: A circular chain of two or more threads exists, where each thread is waiting for a resource held by the next thread in the chain.

Step into the future of tech with these top-rated AI and data science courses:

Master’s in Data Science – LJMU

• PG Diploma in ML & AI – IIIT Bangalore 

Thread Deadlock in Java: Common Scenarios

Thread deadlock in Java commonly occurs in the following scenarios:

1. Nested Synchronized Blocks with Different Lock Order

When multiple threads acquire locks in different orders, they can create a situation where each thread holds a lock that another thread needs to proceed.

2. Database Deadlocks

When multiple transactions lock rows in different orders, database-level deadlocks can occur in JDBC operations.

3. Thread Joins

Improper use of Thread.join() method can create deadlocks if threads wait for each other in a circular manner.

4. Resource Pool Deadlocks

When threads in a resource pool are waiting for resources that will never become available.

Must explore: Comprehensive Guide to Exception Handling in Java: Best Practices and Examples

Deadlock Example in Java

Let's examine a classic deadlock example in Java where two threads hold one resource each and wait for the other resource:

public class DeadlockExample {
    // Resource objects
    private static final Object RESOURCE_1 = new Object();
    private static final Object RESOURCE_2 = new Object();
    
    public static void main(String[] args) {
        // First thread - acquires RESOURCE_1, then tries to acquire RESOURCE_2
        Thread thread1 = new Thread(() -> {
            synchronized (RESOURCE_1) {
                System.out.println("Thread 1: Holding RESOURCE_1...");
                
                try {
                    // Adding delay to ensure the deadlock occurs
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                System.out.println("Thread 1: Waiting for RESOURCE_2...");
                synchronized (RESOURCE_2) {
                    System.out.println("Thread 1: Holding RESOURCE_1 and RESOURCE_2");
                }
            }
        });
        
        // Second thread - acquires RESOURCE_2, then tries to acquire RESOURCE_1
        Thread thread2 = new Thread(() -> {
            synchronized (RESOURCE_2) {
                System.out.println("Thread 2: Holding RESOURCE_2...");
                
                try {
                    // Adding delay to ensure the deadlock occurs
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                System.out.println("Thread 2: Waiting for RESOURCE_1...");
                synchronized (RESOURCE_1) {
                    System.out.println("Thread 2: Holding RESOURCE_2 and RESOURCE_1");
                }
            }
        });
        
        // Start both threads
        thread1.start();
        thread2.start();
    }
}

Expected Output:

Thread 1: Holding RESOURCE_1...
Thread 2: Holding RESOURCE_2...
Thread 1: Waiting for RESOURCE_2...
Thread 2: Waiting for RESOURCE_1...

Explanation: In this example, the program will hang indefinitely because:

  1. Thread 1 acquires RESOURCE_1 and then attempts to acquire RESOURCE_2
  2. Thread 2 acquires RESOURCE_2 and then attempts to acquire RESOURCE_1
  3. Neither thread can proceed since each is waiting for a resource held by the other

Also explore: Thread Lifecycle In Java

Deadlock in Java Multithreading: Detection Techniques

Detecting deadlocks is a crucial aspect of multithreaded application development. Here are some techniques for deadlock detection:

1. Using JVM Tools

Java provides built-in tools to detect deadlocks:
// Example using ThreadMXBean to detect deadlocks
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;

public class DeadlockDetector {
    public static void checkForDeadlocks() {
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        long[] threadIds = threadMXBean.findDeadlockedThreads();
        
        if (threadIds != null) {
            System.out.println("Deadlock detected!");
            // Get more info about the deadlocked threads if needed
            threadMXBean.getThreadInfo(threadIds).forEach(System.out::println);
        }
    }
}

Expected Output (when deadlock occurs):

Deadlock detected!

[Thread details would be displayed here]

Explanation: This code uses the ThreadMXBean from the java.lang.management package to detect deadlocked threads. The findDeadlockedThreads() method returns an array of thread IDs that are deadlocked, or null if no deadlock is detected.

Check out: Ultimate Guide to Synchronization in Java

2. Using Visualization Tools

Tools like JConsole, VisualVM, and Java Mission Control can visualize thread states and detect deadlocks in running applications.

3. Thread Dumps Analysis

Taking thread dumps using jstack utility and analyzing them:

jstack <process-id> > thread-dump.txt

Also read: Difference between Multithreading and Multitasking in Java

How to Avoid Deadlock in Java

Preventing deadlocks is better than trying to recover from them. Here are some effective strategies:

1. Lock Ordering

Always acquire locks in a consistent, predetermined order:

public class DeadlockPrevention {
    private static final Object RESOURCE_1 = new Object();
    private static final Object RESOURCE_2 = new Object();
    
    public static void main(String[] args) {
        // Both threads acquire locks in the same order: RESOURCE_1, then RESOURCE_2
        Thread thread1 = new Thread(() -> {
            synchronized (RESOURCE_1) {
                System.out.println("Thread 1: Holding RESOURCE_1...");
                
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                synchronized (RESOURCE_2) {
                    System.out.println("Thread 1: Holding RESOURCE_1 and RESOURCE_2");
                }
            }
        });
        
        Thread thread2 = new Thread(() -> {
            synchronized (RESOURCE_1) {  // Note: Both threads now acquire RESOURCE_1 first
                System.out.println("Thread 2: Holding RESOURCE_1...");
                
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                synchronized (RESOURCE_2) {
                    System.out.println("Thread 2: Holding RESOURCE_1 and RESOURCE_2");
                }
            }
        });
        
        thread1.start();
        thread2.start();
    }
}

Expected Output:

Thread 1: Holding RESOURCE_1...
Thread 1: Holding RESOURCE_1 and RESOURCE_2
Thread 2: Holding RESOURCE_1...
Thread 2: Holding RESOURCE_1 and RESOURCE_2

Explanation: In this improved version, both threads acquire resources in the same order (RESOURCE_1, then RESOURCE_2), which prevents the circular wait condition necessary for a deadlock.

Also read: Java Interfaces

2. Using Lock Timeout

Use timed lock attempts instead of indefinite waits:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class LockTimeoutExample {
    private static final Lock LOCK_1 = new ReentrantLock();
    private static final Lock LOCK_2 = new ReentrantLock();
    
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            boolean acquired1 = false;
            boolean acquired2 = false;
            
            try {
                acquired1 = LOCK_1.tryLock(1, TimeUnit.SECONDS);
                if (acquired1) {
                    System.out.println("Thread 1: Acquired LOCK_1");
                    
                    // Try to get the second lock
                    acquired2 = LOCK_2.tryLock(1, TimeUnit.SECONDS);
                    if (acquired2) {
                        System.out.println("Thread 1: Acquired LOCK_2");
                        // Do work here
                    } else {
                        System.out.println("Thread 1: Failed to acquire LOCK_2. Releasing LOCK_1");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // Release locks in reverse order of acquisition
                if (acquired2) LOCK_2.unlock();
                if (acquired1) LOCK_1.unlock();
            }
        });
        
        // Similar logic for thread2
        thread1.start();
    }
}

Expected Output:

Thread 1: Acquired LOCK_1
Thread 1: Acquired LOCK_2

or

Thread 1: Acquired LOCK_1
Thread 1: Failed to acquire LOCK_2. Releasing LOCK_1

Explanation: Using tryLock() with a timeout prevents indefinite waiting by allowing the thread to give up after a specified time if it cannot acquire the lock.

3. Using Concurrent Collections

Java's concurrent collections are designed to prevent deadlocks:

import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;

public class ConcurrentCollectionExample {
    public static void main(String[] args) {
        // Thread-safe map that doesn't require external synchronization
        Map<String, String> concurrentMap = new ConcurrentHashMap<>();
        
        Thread thread1 = new Thread(() -> {
            concurrentMap.put("key1", "value1");
            System.out.println("Thread 1: " + concurrentMap.get("key2"));
        });
        
        Thread thread2 = new Thread(() -> {
            concurrentMap.put("key2", "value2");
            System.out.println("Thread 2: " + concurrentMap.get("key1"));
        });
        
        thread1.start();
        thread2.start();
    }
}

Expected Output:

Thread 1: value2
Thread 2: value1

or

Thread 2: value1
Thread 1: value2

Explanation: ConcurrentHashMap allows multiple threads to read and write concurrently without external synchronization, eliminating a common source of deadlocks.

4. Using Thread Join with Timeout

When using Thread.join(), include a timeout:

public class ThreadJoinTimeout {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            try {
                System.out.println("Thread 1 is running");
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        thread1.start();
        
        try {
            // Wait for thread1 to complete, but with a timeout
            thread1.join(3000);  // Wait for max 3 seconds
            
            if (thread1.isAlive()) {
                System.out.println("Thread 1 is still running after timeout");
            } else {
                System.out.println("Thread 1 completed within the timeout");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Expected Output:

Thread 1 is running
Thread 1 completed within the timeout

Explanation: Using join() with a timeout ensures that the main thread won't wait indefinitely if the other thread gets stuck.

Advanced Deadlock Handling Techniques

1. Deadlock Recovery

In some scenarios, it's possible to implement deadlock recovery mechanisms:

import java.util.concurrent.atomic.AtomicInteger;

public class DeadlockRecovery {
    private static final Object RESOURCE_A = new Object();
    private static final Object RESOURCE_B = new Object();
    private static final AtomicInteger attempts = new AtomicInteger(0);
    
    public static void main(String[] args) {
        Thread watchdog = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(5000); // Check every 5 seconds
                    
                    // If multiple attempts have been made, assume deadlock
                    if (attempts.get() > 3) {
                        System.out.println("Potential deadlock detected! Interrupting threads...");
                        
                        // Find and interrupt deadlocked threads
                        Thread.getAllStackTraces().keySet().forEach(thread -> {
                            if (thread.getState() == Thread.State.BLOCKED) {
                                System.out.println("Interrupting blocked thread: " + thread.getName());
                                thread.interrupt();
                            }
                        });
                        
                        attempts.set(0);
                    }
                } catch (InterruptedException e) {
                    break;
                }
            }
        });
        
        watchdog.setDaemon(true);
        watchdog.start();
        
        // Your potentially deadlocking code here
    }
}

Expected Output:

Potential deadlock detected! Interrupting threads...
Interrupting blocked thread: Thread-1
Interrupting blocked thread: Thread-2

Explanation: This watchdog thread monitors for potential deadlocks by tracking the number of attempts to acquire resources and interrupts threads that appear to be deadlocked.

2. Resource Hierarchy

Organize resources in a hierarchy and always acquire them in hierarchical order:

class Account {
    private final int id;
    private double balance;
    
    public Account(int id, double initialBalance) {
        this.id = id;
        this.balance = initialBalance;
    }
    
    public int getId() {
        return id;
    }
    
    public void withdraw(double amount) {
        balance -= amount;
    }
    
    public void deposit(double amount) {
        balance += amount;
    }
}

public class HierarchicalLocking {
    public static void transfer(Account fromAccount, Account toAccount, double amount) {
        // Always lock accounts in order of their ID to prevent deadlocks
        Account firstLock = fromAccount.getId() < toAccount.getId() ? fromAccount : toAccount;
        Account secondLock = fromAccount.getId() < toAccount.getId() ? toAccount : fromAccount;
        
        synchronized (firstLock) {
            System.out.println("Acquired lock on account: " + firstLock.getId());
            
            synchronized (secondLock) {
                System.out.println("Acquired lock on account: " + secondLock.getId());
                
                // Perform the actual transfer
                if (fromAccount == firstLock) {
                    fromAccount.withdraw(amount);
                    toAccount.deposit(amount);
                } else {
                    toAccount.deposit(amount);
                    fromAccount.withdraw(amount);
                }
                
                System.out.println("Transfer complete");
            }
        }
    }
    
    public static void main(String[] args) {
        final Account account1 = new Account(1, 1000);
        final Account account2 = new Account(2, 2000);
        
        // Both threads will acquire locks in the same order (account1 then account2)
        // regardless of which is the source/destination account
        new Thread(() -> transfer(account1, account2, 100)).start();
        new Thread(() -> transfer(account2, account1, 200)).start();
    }
}

Expected Output:

Acquired lock on account: 1
Acquired lock on account: 2
Transfer complete
Acquired lock on account: 1
Acquired lock on account: 2
Transfer complete

Explanation: This implementation ensures that locks are always acquired in a consistent order (by account ID) regardless of the transfer direction, preventing deadlocks.

Conclusion

Deadlock in Java remains one of the most challenging aspects of concurrent programming. By understanding the conditions that cause deadlocks and implementing proper prevention techniques such as consistent lock ordering, timeouts, and resource hierarchies, you can develop more robust multithreaded applications.

 Remember that proactive prevention is far more effective than trying to detect and recover from deadlocks after they occur. Regularly testing your application under high concurrency conditions will help identify potential deadlock scenarios before they impact your production environment.

FAQ about Deadlock in Java

What is the main cause of deadlock in Java?

Deadlock in Java primarily occurs when two or more threads each hold resources that the other needs and neither is willing to release their held resources. The circular wait condition, combined with mutual exclusion, hold-and-wait, and no preemption, creates a situation where threads are perpetually waiting for each other.

How can I identify a deadlock in a running Java application?

You can identify deadlocks in a running Java application using tools like jstack to generate thread dumps, JConsole or VisualVM for visual monitoring, or programmatically using ThreadMXBean's findDeadlockedThreads() method to detect deadlocked threads.

Can deadlocks occur in single-threaded applications?

No, deadlocks are specific to multi-threaded environments. Single-threaded applications execute sequentially, so there's no possibility of threads waiting for each other's resources in a circular manner.

What is the difference between deadlock and starvation in Java?

Deadlock occurs when threads are permanently blocked waiting for resources held by each other. Starvation occurs when a thread is unable to gain regular access to shared resources, preventing progress, but the system as a whole continues functioning.

Can Java automatically detect and resolve deadlocks?

The JVM can detect deadlocks but doesn't automatically resolve them. Detection is possible through ThreadMXBean or thread dumps, but resolution must be programmatically implemented by the developer.

What are the four necessary conditions for a deadlock?

The four necessary conditions are: (1) Mutual Exclusion - resources cannot be shared simultaneously, (2) Hold and Wait - threads hold resources while waiting for others, (3) No Preemption - resources cannot be forcibly taken away, and (4) Circular Wait - a circular chain of waiting threads exists.

How does lock timeout help prevent deadlocks?

Lock timeouts limit how long a thread will wait to acquire a lock. If the timeout expires, the thread can release any held locks and retry later, potentially breaking the hold-and-wait condition necessary for deadlocks.

Can deadlocks occur with Java's synchronized keyword?

Yes, improper use of synchronized blocks or methods is one of the most common causes of deadlocks in Java, particularly when multiple locks are acquired in different orders by different threads.

Are java.util.concurrent locks better at preventing deadlocks than synchronized?

The concurrent package locks offer more sophisticated features like tryLock() with timeouts and interruptible lock acquisition, which can help prevent or recover from potential deadlock situations, unlike synchronized which blocks indefinitely.

What is livelock and how is it different from deadlock?

Livelock occurs when threads continuously change their state in response to each other without making progress. Unlike deadlock where threads are blocked, in livelock, threads are actively performing actions but cannot complete their task.

How can I prevent deadlocks when working with database transactions?

To prevent database deadlocks: (1) Keep transactions short, (2) Access tables in a consistent order, (3) Use appropriate isolation levels, (4) Add deadlock detection/retry logic, and (5) Consider using optimistic locking where appropriate

image

Take the Free Quiz on Java

Answer quick questions and assess your Java knowledge

right-top-arrow
image
Join 10M+ Learners & Transform Your Career
Learn on a personalised AI-powered platform that offers best-in-class content, live sessions & mentorship from leading industry experts.
advertise-arrow

Free Courses

Explore Our Free Software Tutorials

upGrad Learner Support

Talk to our experts. We are available 7 days a week, 9 AM to 12 AM (midnight)

text

Indian Nationals

1800 210 2020

text

Foreign Nationals

+918068792934

Disclaimer

1.The above statistics depend on various factors and individual results may vary. Past performance is no guarantee of future results.

2.The student assumes full responsibility for all expenses associated with visas, travel, & related costs. upGrad does not provide any a.