top

Search

Java Tutorial

.

UpGrad

Java Tutorial

Deadlock in Java

Introduction

A deadlock in Java is crucial in concurrent programming. They can make a huge impact on the performance and stability of applications. In Java, a deadlock arises when two or more threads are stuck indefinitely, awaiting the release of resources held by each other. Java developers must understand deadlocks and possess knowledge of techniques to prevent and resolve them effectively.

Overview

This tutorial aims to provide a comprehensive understanding of deadlock in Java with examples, how to detect deadlock in Java, and ways to avoid them. 

What is a Deadlock in Java Program? 

A deadlock is a scenario in concurrent programming where multiple threads are stuck waiting for each other, leading to a complete standstill. It happens when each thread holds a resource while waiting for another one held by a different thread. As a result, none of the threads can progress, causing the program's execution to halt.

Let us learn what is a deadlock in Java with example programs.

public class upGradTutorials {
    private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("Thread 1 acquired resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (resource2) {
                    System.out.println("Thread 1 acquired resource2");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (resource2) {
                System.out.println("Thread 2 acquired resource2");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (resource1) {
                    System.out.println("Thread 2 acquired resource1");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

In this example, two threads (thread1 and thread2) that attempt to acquire two different resources (resource1 and resource2) in a specific order. However, the order of acquisition of resources in each thread is different. This creates a potential for a deadlock situation.

When thread1 starts executing, it acquires resource1 and then pauses for a while using Thread.sleep(1000) to simulate some work being done. Meanwhile, thread2 starts executing and acquires resource2. Both threads are waiting for the other resource to be released, causing a deadlock.

If you run this program, you'll notice it gets stuck and does not terminate. This is because thread1 is holding onto resource1 and waiting for resource2, while thread2 is holding onto resource2 and waiting for resource1. Hence, both threads are deadlocked, unable to proceed further.

More Complicated Deadlocks

Here is a more complicated example of a deadlock in Java involving multiple threads and shared resources:

public class upGradTutorials {
    private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("Thread 1 acquired resource1");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (resource2) {
                    System.out.println("Thread 1 acquired resource2");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (resource2) {
                System.out.println("Thread 2 acquired resource2");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (resource1) {
                    System.out.println("Thread 2 acquired resource1");
                }
            }
        });

        Thread thread3 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("Thread 3 acquired resource1");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (resource2) {
                    System.out.println("Thread 3 acquired resource2");
                }
            }
        });

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

In this example, three threads (thread1, thread2, and thread3) that attempt to acquire two different resources (resource1 and resource2) in a specific order. However, the order of acquisition of resources in each thread is different, leading to a deadlock situation.

When thread1 starts executing, it acquires resource1 and then pauses using Thread.sleep(1000). Meanwhile, thread2 starts executing and acquires resource2. Similarly, thread3 starts executing and acquires resource1. All three threads are waiting for the other resources to be released, causing a deadlock.

If you run this program, you'll notice it gets stuck and does not terminate. This is because thread1 is holding onto resource1 and waiting for resource2thread2 is holding onto resource2 and waiting for resource1, and thread3 is holding onto resource1 and waiting for resource2. Hence, all three threads are deadlocked, unable to proceed further.

How to Detect Dead Lock in Java?

To detect deadlocks in Java, you can employ the following approaches:-

  • Analyze a thread dump of your running Java application to identify threads in a BLOCKED or WAITING state, indicating potential deadlocks.

  • Use the jcmd command-line tool or other profilers to obtain thread information and detect deadlocks.

  • Utilize Java Management Extensions (JMX) to monitor the application state and periodically check for deadlock conditions using the ThreadMXBean interface.

  • Consider using third-party tools and profilers that offer dedicated deadlock detection features. 

Conditions for Deadlock 

Deadlocks usually arise when the following four conditions coincide simultaneously:

Mutual Exclusion

This condition states that at least one resource must be exclusively allocated to one thread at a time. It means that when a thread uses a particular resource, other threads are prevented from concurrently accessing or using the same resource. 

Hold and Wait

Hold and wait refers to a situation where a thread holds a resource while waiting for another resource currently held by a different thread. It introduces a dependency chain among the threads, where each thread requires a resource held by another thread.

If the threads do not release their held resources, a deadlock can occur as they wait indefinitely for the resources to become available.

Example:

public class upGradTutorials {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1 acquired lock1");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (lock2) {
                    System.out.println("Thread 1 acquired lock2");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2 acquired lock2");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (lock1) {
                    System.out.println("Thread 2 acquired lock1");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

No Preemption

The no preemption condition means that resources cannot be forcefully taken away from threads. This condition can contribute to deadlocks because if a thread is holding a resource that another thread requires, it must wait for the first thread to release the resource, even if the second thread could use it more efficiently.

Circular Wait

Circular wait involves a circular chain of two or more threads, where each thread waits for a resource held by another thread. This circular dependency among the threads' resource requirements creates a deadlock, as no thread in the chain can proceed until it receives the resource held by another thread. 

How to Avoid Deadlock in Java? 

To avoid deadlocks in Java, you can follow several practices. Here are three techniques that can help prevent deadlocks:

Avoid Nested Locks

If you acquire multiple locks, ensure they are always in the same order across all threads. This practice helps prevent different threads from acquiring locks in different orders, potentially leading to deadlocks. 

public class upGradTutorials {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1 acquired lock1");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (lock2) {
                    System.out.println("Thread 1 acquired lock2");
                    // Perform some operations using lock1 and lock2
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2 acquired lock2");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (lock1) {
                    System.out.println("Thread 2 acquired lock1");
                    // Perform some operations using lock1 and lock2
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

In this example, we have two threads (thread1 and thread2) that acquire locks lock1 and lock2, respectively. The critical point is that each thread acquires its respective lock before attempting to acquire the other lock. By ensuring a consistent order of lock acquisition across threads, we eliminate the possibility of nested locks and potential deadlocks.

Avoid Unnecessary Locks

Avoid acquiring locks when unnecessary, as unnecessary locking can increase the chances of deadlocks. If a resource doesn't require shared access among threads, consider using other synchronization mechanisms that do not rely on locks, such as atomic variables or concurrent collections.

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

public class upGradTutorials {
    private static final Lock lock = new ReentrantLock();
    private static int counter = 0;

    public static void main(String[] args) {
        Thread incrementThread = new Thread(() -> {
            lock.lock();
            try {
                // Perform increment operation
                counter++;
            } finally {
                lock.unlock();
            }
        });

        Thread decrementThread = new Thread(() -> {
            // No need to acquire the lock for decrement operation

            // Perform decrement operation
            counter--;
        });

        incrementThread.start();
        decrementThread.start();
    }
}

In this example, we have two threads (incrementThread and decrementThread) that perform increment and decrement operations on a shared counter variable. Notice that the decrementThread does not acquire the lock because it does not require synchronization with the incrementThread

Using Thread Joins

When working with multiple threads, utilize the join() method to ensure proper sequencing and synchronization. The join() method allows a thread to wait until another completes its execution. By using join() appropriately, you can avoid situations where a thread waits indefinitely for another thread to finish, reducing the chances of deadlocks.

public class upGradTutorials {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            System.out.println("Thread 1 started");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread 1 completed");
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("Thread 2 started");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread 2 completed");
        });

        thread1.start();
        thread2.start();

        try {
            // Wait for thread1 to complete
            thread1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Main thread completed");
    }
}

In this example, we have two threads (thread1 and thread2) that perform some operations and then complete their execution. The main thread starts both threads and then calls the join() method on thread1 to wait for its completion. This ensures that the main thread does not proceed further until thread1 has finished executing. Once thread1 completes, the main thread resumes execution and prints "Main thread completed" to the console.

Important Points

When dealing with deadlocks in Java, keep the following points in mind:

  • Understand the necessary conditions for deadlock: Mutual Exclusion, Hold and Wait, No Preemption, and Circular Wait.

  • Employ prevention strategies like avoiding nested locks, minimizing unnecessary locks, and enforcing a consistent lock acquisition order.

  • Use techniques such as thread join to ensure proper sequencing and synchronization.

  • Implement deadlock detection mechanisms to identify and resolve deadlocks promptly.

  • Continuously review and improve your synchronization and resource allocation practices.

  • Understand the principles of concurrent programming and strive for thread-safe designs.

Conclusion

Understanding and addressing a deadlock in Java is crucial for developing robust and efficient Java applications. You can mitigate the risk of deadlocks by following best practices such as avoiding circular wait, minimizing unnecessary locks, and using appropriate synchronization techniques. 

To master the art of handling such scenarios in programming, you can sign up for a comprehensive course offered by upGrad

FAQs

1. How to handle a deadlock in Java?

Deadlocks can be handled by implementing prevention, detection, and resolution techniques (resource ordering, deadlock detection algorithms, etc.).

2. How can a deadlock be cleared?

Deadlocks can be cleared by releasing held resources, restarting affected threads, or redesigning the program's synchronization and resource usage to break the circular dependency causing the deadlock.

3. What is a synchronization deadlock in Java?

A synchronization deadlock in Java occurs when multiple threads are blocked and waiting for each other to release resources or locks, resulting in a situation where none of the threads can progress. It happens when the conditions of mutual exclusion, hold and wait, no preemption, and circular wait are satisfied.

Leave a Reply

Your email address will not be published. Required fields are marked *