How to avoid deadlock in Java?

Free Coding Questions Catalog
Boost your coding skills with our essential coding questions catalog. Take a step towards a better tech career now!

Deadlock is a situation in multithreading where two or more threads are blocked forever, waiting for each other to release resources. This can occur when each thread holds a lock on one resource and waits to acquire a lock on the resource held by another thread. Deadlock can severely impact the performance and behavior of your program.

In Java, deadlocks can be avoided by following a few key strategies. Below are some common approaches to avoid deadlocks:

1. Avoid Nested Locks (Lock Ordering)

Deadlocks often occur when multiple threads acquire locks in different orders. To avoid this, always acquire locks in a consistent order. This ensures that threads don’t wait indefinitely for a lock held by another thread that is waiting for a lock held by the first thread.

Example:

If you have two resources (say Resource A and Resource B), always acquire the locks in the same order. For example, always lock A first and then lock B (never the other way around).

class Thread1 extends Thread { private final Object lockA; private final Object lockB; public Thread1(Object lockA, Object lockB) { this.lockA = lockA; this.lockB = lockB; } public void run() { synchronized (lockA) { // Lock A System.out.println("Thread1: Locked A"); synchronized (lockB) { // Lock B System.out.println("Thread1: Locked B"); } } } } class Thread2 extends Thread { private final Object lockA; private final Object lockB; public Thread2(Object lockA, Object lockB) { this.lockA = lockA; this.lockB = lockB; } public void run() { synchronized (lockA) { // Lock A System.out.println("Thread2: Locked A"); synchronized (lockB) { // Lock B System.out.println("Thread2: Locked B"); } } } }

In this example, if both threads lock lockA first and then lockB, they avoid deadlock by following the same locking order. If Thread1 locks lockA first and Thread2 locks lockB first, they will wait indefinitely.

2. Use Try Lock Instead of Blocked Lock

Java provides the ReentrantLock class in the java.util.concurrent.locks package, which offers a tryLock() method. This method tries to acquire the lock, but if the lock is not available, it returns false immediately rather than blocking the thread indefinitely. This way, you can attempt to acquire the lock without waiting forever.

import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class TryLockExample { private final Lock lockA = new ReentrantLock(); private final Lock lockB = new ReentrantLock(); public void method1() { if (lockA.tryLock()) { try { System.out.println("Locked A in method1"); if (lockB.tryLock()) { try { System.out.println("Locked B in method1"); } finally { lockB.unlock(); } } } finally { lockA.unlock(); } } } public void method2() { if (lockB.tryLock()) { try { System.out.println("Locked B in method2"); if (lockA.tryLock()) { try { System.out.println("Locked A in method2"); } finally { lockA.unlock(); } } } finally { lockB.unlock(); } } } }

In this case, tryLock() prevents threads from blocking indefinitely. If a thread cannot acquire the lock, it can take alternative action (e.g., retry, exit, or log the failure).

3. Use Timed Locks (Timeout)

You can also use timed locks to limit how long a thread will wait for a lock. This is achieved using tryLock(long time, TimeUnit unit), which tries to acquire the lock within a specified time period. If the lock is not acquired within the time limit, the thread proceeds with other tasks instead of waiting indefinitely.

import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.TimeUnit; public class TimedLockExample { private final Lock lockA = new ReentrantLock(); private final Lock lockB = new ReentrantLock(); public void method1() { try { if (lockA.tryLock(100, TimeUnit.MILLISECONDS)) { try { System.out.println("Locked A in method1"); if (lockB.tryLock(100, TimeUnit.MILLISECONDS)) { try { System.out.println("Locked B in method1"); } finally { lockB.unlock(); } } } finally { lockA.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); // Handle interruption } } public void method2() { try { if (lockB.tryLock(100, TimeUnit.MILLISECONDS)) { try { System.out.println("Locked B in method2"); if (lockA.tryLock(100, TimeUnit.MILLISECONDS)) { try { System.out.println("Locked A in method2"); } finally { lockA.unlock(); } } } finally { lockB.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); // Handle interruption } } }

4. Use Deadlock Detection and Recovery

You can implement deadlock detection by periodically checking the system for deadlocks. This approach is more complex and often involves tracking the state of all threads and locks. Java provides some tools to detect deadlocks using ThreadMXBean in the java.lang.management package.

import java.lang.management.ManagementFactory; import java.lang.management.ThreadMXBean; public class DeadlockDetectionExample { public static void detectDeadlock() { ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); long[] deadlockedThreads = threadMXBean.findDeadlockedThreads(); if (deadlockedThreads != null) { System.out.println("Deadlock detected!"); for (long threadId : deadlockedThreads) { System.out.println("Deadlocked thread ID: " + threadId); } } } }

While deadlock detection can identify issues, it doesn’t prevent them and may require some form of recovery (such as interrupting threads or rolling back operations).

5. Use Higher-Level Concurrency Utilities

Java’s java.util.concurrent package provides higher-level utilities (such as ExecutorService, Semaphore, CyclicBarrier, and CountDownLatch) that abstract much of the locking and synchronization complexity, making it easier to avoid situations that could lead to deadlock.

Conclusion

To avoid deadlock in Java:

  • Always acquire locks in a consistent order to prevent circular waiting.
  • Use tryLock() or tryLock(long time, TimeUnit unit) to prevent threads from waiting indefinitely.
  • Implement timeouts and avoid waiting forever for resources.
  • Use deadlock detection to identify potential issues, but avoid relying solely on this.
  • Use higher-level concurrency utilities from the java.util.concurrent package to manage concurrency effectively.

To learn more about concurrency and multithreading in Java and prepare for related interview questions, consider exploring these courses from DesignGurus.io:

These courses provide comprehensive guidance on multithreading, concurrency, and related concepts, helping you excel in your interviews and practical applications.

TAGS
Coding Interview
System Design Interview
CONTRIBUTOR
Design Gurus Team

GET YOUR FREE

Coding Questions Catalog

Design Gurus Newsletter - Latest from our Blog
Boost your coding skills with our essential coding questions catalog.
Take a step towards a better tech career now!
Explore Answers
Is ISO an industry standard?
What are the top 5 frontend frameworks 2024?
Is Stripe interview difficult?
Related Courses
Image
Grokking the Coding Interview: Patterns for Coding Questions
Grokking the Coding Interview Patterns in Java, Python, JS, C++, C#, and Go. The most comprehensive course with 476 Lessons.
Image
Grokking Data Structures & Algorithms for Coding Interviews
Unlock Coding Interview Success: Dive Deep into Data Structures and Algorithms.
Image
Grokking Advanced Coding Patterns for Interviews
Master advanced coding patterns for interviews: Unlock the key to acing MAANG-level coding questions.
Image
One-Stop Portal For Tech Interviews.
Copyright © 2024 Designgurus, Inc. All rights reserved.