Deadlock Prevention in Java


Deadlock is a situation in multithreading where two or more threads are waiting for each other to release resources, causing a standstill. Deadlock prevention is crucial for ensuring the smooth execution of concurrent applications.

Step 1: Understanding Deadlock

Below is an example of a potential deadlock situation:

    class Resource {
        void method1(Resource other) {
            synchronized (this) {
                System.out.println(Thread.currentThread().getName() + " locked this resource");
                synchronized (other) {
                    System.out.println(Thread.currentThread().getName() + " locked the other resource");
                }
            }
        }
    }
    
    public class DeadlockExample {
        public static void main(String[] args) {
            Resource r1 = new Resource();
            Resource r2 = new Resource();
    
            Thread t1 = new Thread(() -> r1.method1(r2), "Thread-1");
            Thread t2 = new Thread(() -> r2.method1(r1), "Thread-2");
    
            t1.start();
            t2.start();
        }
    }
        

In this example, Thread-1 locks r1 and waits for r2, while Thread-2 locks r2 and waits for r1. This causes a deadlock.

Step 2: Deadlock Prevention Techniques

To prevent deadlocks, you can follow these strategies:

1. Avoid Nested Locks

Try to avoid acquiring locks within locks. Below is a revised example to avoid nested locks:

    class Resource {
        synchronized void method() {
            System.out.println(Thread.currentThread().getName() + " is using this resource");
        }
    }
    
    public class AvoidNestedLocks {
        public static void main(String[] args) {
            Resource r1 = new Resource();
            Resource r2 = new Resource();
    
            Thread t1 = new Thread(() -> {
                synchronized (r1) {
                    System.out.println("Thread-1 locked r1");
                    r1.method();
                }
                synchronized (r2) {
                    System.out.println("Thread-1 locked r2");
                    r2.method();
                }
            });
    
            Thread t2 = new Thread(() -> {
                synchronized (r2) {
                    System.out.println("Thread-2 locked r2");
                    r2.method();
                }
                synchronized (r1) {
                    System.out.println("Thread-2 locked r1");
                    r1.method();
                }
            });
    
            t1.start();
            t2.start();
        }
    }
        

2. Use a Lock Ordering System

Always acquire locks in a fixed, consistent order to avoid circular dependencies:

    class Resource {
        synchronized void use() {
            System.out.println(Thread.currentThread().getName() + " is using this resource");
        }
    }
    
    public class LockOrderingExample {
        public static void main(String[] args) {
            Resource r1 = new Resource();
            Resource r2 = new Resource();
    
            Thread t1 = new Thread(() -> {
                synchronized (r1) {
                    System.out.println("Thread-1 locked r1");
                    synchronized (r2) {
                        System.out.println("Thread-1 locked r2");
                        r2.use();
                    }
                }
            });
    
            Thread t2 = new Thread(() -> {
                synchronized (r1) {
                    System.out.println("Thread-2 locked r1");
                    synchronized (r2) {
                        System.out.println("Thread-2 locked r2");
                        r2.use();
                    }
                }
            });
    
            t1.start();
            t2.start();
        }
    }
        

3. Use Try-Lock Mechanism

Java's ReentrantLock provides a try-lock mechanism to avoid deadlocks:

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    class Resource {
        private final Lock lock = new ReentrantLock();
    
        public boolean tryLock() {
            return lock.tryLock();
        }
    
        public void unlock() {
            lock.unlock();
        }
    
        public void use() {
            System.out.println(Thread.currentThread().getName() + " is using the resource");
        }
    }
    
    public class TryLockExample {
        public static void main(String[] args) {
            Resource r1 = new Resource();
            Resource r2 = new Resource();
    
            Thread t1 = new Thread(() -> {
                if (r1.tryLock()) {
                    try {
                        System.out.println("Thread-1 locked r1");
                        if (r2.tryLock()) {
                            try {
                                System.out.println("Thread-1 locked r2");
                                r2.use();
                            } finally {
                                r2.unlock();
                            }
                        }
                    } finally {
                        r1.unlock();
                    }
                }
            });
    
            Thread t2 = new Thread(() -> {
                if (r2.tryLock()) {
                    try {
                        System.out.println("Thread-2 locked r2");
                        if (r1.tryLock()) {
                            try {
                                System.out.println("Thread-2 locked r1");
                                r1.use();
                            } finally {
                                r1.unlock();
                            }
                        }
                    } finally {
                        r2.unlock();
                    }
                }
            });
    
            t1.start();
            t2.start();
        }
    }
        

Summary

In this tutorial, you learned:

  • What deadlock is and how it occurs
  • How to prevent deadlocks by avoiding nested locks
  • How to implement lock ordering to avoid circular dependencies
  • How to use the try-lock mechanism with ReentrantLock

By following these techniques, you can prevent deadlocks in Java applications and ensure smooth multithreading execution.





Advertisement