Troubleshooting Deadlock in Java

Upasana | August 11, 2020 | 4 min read | 2 views


Deadlock

Deadlock describes a situation where two or more threads waiting on two or more shared resource in a particular order are blocked forever, waiting for each other to complete. Deadlocks can occur in Java when the synchronized keyword causes the executing thread to block while waiting to get the lock, associated with the specified object. Since the thread might already hold locks associated with other objects, two threads could each be waiting for the other to release a lock. In such case, they will end up waiting forever.

Java Source for producing a Deadlock Condition
package com.shunya.tutorials;

public class DeadLock {

    String resource1 = "Resource1";
    String resource2 = "Resource2";

    public void thread1Work() {
        Thread t1 = new Thread(() -> {
            while (true) {
                synchronized (resource1) {
                    synchronized (resource2) {
                        System.out.println(resource1 + resource2);
                    }
                }
            }
        });
        t1.start();
    }

    public void thread2Work() {
        Thread t2 = new Thread(() -> {
            while (true) {
                synchronized (resource2) {
                    synchronized (resource1) {
                        System.out.println(resource1 + resource2);
                    }
                }
            }
        });
        t2.start();
    }

    public static void main(String[] args) {
        DeadLock deadLock = new DeadLock();
        deadLock.thread1Work();
        deadLock.thread2Work();
    }
}

Using VisualVM to troubleshoot Deadlock Condition

Java Visual VM is a tool bundled with standard Java Development Kit

deadlock jvisualvm
Stacktrace shown in JVisualVM for Deadlock Condition
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007f6228003828 (object 0x0000000771000720, a java.lang.String),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00007f6228004e28 (object 0x0000000771000738, a java.lang.String),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at com.shunya.tutorials.DeadLock.lambda$thread2Work$1(DeadLock.java:26)
        - waiting to lock <0x0000000771000720> (a java.lang.String)
        - locked <0x0000000771000738> (a java.lang.String)
        at com.shunya.tutorials.DeadLock$$Lambda$2/1831932724.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
"Thread-0":
        at com.shunya.tutorials.DeadLock.lambda$thread1Work$0(DeadLock.java:13)
        - waiting to lock <0x0000000771000738> (a java.lang.String)
        - locked <0x0000000771000720> (a java.lang.String)
        at com.shunya.tutorials.DeadLock$$Lambda$1/1096979270.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

How to Avoid Deadlock?

You can take few steps to ensure that deadlock does not occur in your application. Two main options for avoiding deadlock are-

  1. All threads must acquire lock on shared objects in same order.

  2. Use Explicit Locking in Java with a timeout specified on the lock so that if the requested resource is not available within time limit, code can proceed further.

  3. We should shrink scope of synchronized blocks to reduce possibility deadlock. This will not avoid deadlock but prevent it to some extent.

We will see example code for both the options-

Acquire Lock in Same Order to avoid deadlock

If we acquire the lock on shared resource in same order from all threads, then deadlock will never occur. But in a real life this is not achievable due to large codebase. In the DeadLock example shown above, if you acquire lock on resource1 and resource2 in same order in both threads, deadlock will never occur.

Use Explicit Locking to avoid deadlock (tryLock() and wait(timeout))

ReentrantLock class provides a method tryLock() that Acquires the lock only if it is free at the time of invocation. This can be used to avoid un-necessary deadlock between two threads. For example,

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

public class DeadlockSolution {

    final Lock lock1 = new ReentrantLock();
    final Lock lock2 = new ReentrantLock();

    public void deadLockDemo() {
        Thread t1 = new Thread(new RunnableA());
        t1.setName("Thread A");
        t1.start();


        Thread t2 = new Thread(new RunnableB());
        t2.setName("Thread B");
        t2.start();
    }

    public static void main(String[] args) {
        new DeadlockSolution().deadLockDemo();
    }

    class RunnableA implements Runnable {

        public void run() {
            boolean done = false;
            while (!done) {
                if (lock1.tryLock()) {
                    try {
                        System.out.println(Thread.currentThread().getName() + ": Got lockObject1. Trying for lockObject2");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                        if (lock2.tryLock()) {
                            try {
                                System.out.println(Thread.currentThread().getName() + ": Got lockObject2.");
                                done = true;
                            } finally {
                                lock2.unlock();
                            }
                        }

                    } finally {
                        lock1.unlock();
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }

    class RunnableB implements Runnable {

        public void run() {
            boolean done = false;
            while (!done) {
                if (lock2.tryLock()) {
                    try {
                        System.out.println(Thread.currentThread().getName() + ": Got lockObject2. Trying for lockObject1");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                        if (lock1.tryLock()) {
                            try {
                                System.out.println(Thread.currentThread().getName() + ": Got lockObject1.");
                                done = true;
                            } finally {
                                lock1.unlock();
                            }
                        }

                    } finally {
                        lock2.unlock();
                        try {
                            Thread.sleep(750);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

We can even specify the timeout value for Lock Acquisition, like below

Lock lock = ...;
if (lock.tryLock(15L, TimeUnit.SECONDS)) {
    try {
       ........
    } finally {
        lock.unlock();
    }
} else {
      // do sumthing
}

This approach should be preferred because it does not require dependency on order of acquiring lock on shared resource, and quite practical solution for a real life project.

References

  1. JVisualVM Tool Shipped with JDK - https://visualvm.github.io/


Top articles in this category:
  1. Explain the threading Jargon in Java
  2. ThreadLocal with examples in Java
  3. Producer Consumer Problem using Blocking Queue in Java
  4. What is Double Checked Locking Problem in Multi-Threading?
  5. Blocking Queue implementation in Java
  6. Custom Thread pool implementation in Java
  7. How will you implement your custom threadsafe Semaphore in Java

Recommended books for interview preparation:

Find more on this topic:
Buy interview books

Java & Microservices interview refresher for experienced developers.