Understanding Deadlock in Java: Causes and Solutions

Understanding Deadlock in Java: Causes and Solutions

Deadlock represents a common problem in concurrent applications. In such applications, we use a locking mechanism to ensure thread safety. Additionally, we use thread pools and semaphores to manage resource consumption. However, in some situations, these techniques can cause deadlock.

In this article, we’ll explore deadlock, why it appears, and how to analyze and avoid potential deadlock situations.

Understanding Deadlock

Simply put, a deadlock occurs when two or more threads block each other while waiting for another resource held by another thread to become available.

The JVM isn’t designed to recover from deadlock. Therefore, depending on what those threads do, when deadlock happens, the entire application may stall, or it can cause performance degradation.

Deadlock Example

To illustrate the deadlock, let’s create a simulation of transferring funds between two accounts:

private static void transferFunds(Account fromAccount, Account toAccount, BigDecimal amount) {
    synchronized (fromAccount) {
        System.out.println(Thread.currentThread().getName() + " acquired lock on " + fromAccount);
        synchronized (toAccount) {
            transfer(fromAccount, toAccount, amount);
        }
    }
}

public static void transfer(Account fromAccount, Account toAccount, BigDecimal amount) {
    if (fromAccount.getBalance().compareTo(amount) < 0)
        throw new RuntimeException("Insufficient funds.");
    else {
        fromAccount.withdraw(amount);
        toAccount.deposit(amount);
        System.out.println(Thread.currentThread()
                .getName() + " transferred $" + amount + " from " + fromAccount + " to " + toAccount);
    }
}
        

At first glance, the code above may not make it obvious how the transferFunds() method can result in a deadlock. It seems like all the threads acquire their locks in the same order. However, the lock order depends on the order of the arguments passed to the transferFunds() method.

In our example, deadlock can occur when two threads call the transferFunds() method at the same time, one transferring the funds from account1 to account2 and the other transferring from account2 to account1:

Thread thread1 = new Thread(() -> transferFunds(account1, account2, BigDecimal.valueOf(500)));
Thread thread2 = new Thread(() -> transferFunds(account2, account1, BigDecimal.valueOf(300)));

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

The thread1 acquires the lock on account1 and waits for the lock on account2, while thread2 holds the lock on account2 and waits for the lock on account1.

Fixing Deadlocks

To fix the deadlock in our example, we can define the ordering of locks and acquire them consistently throughout the application. This way, we can ensure each thread acquires the locks in the same order.

One way to introduce object ordering is to leverage their hashCode value. Furthermore, we can use System.identityHashCode, which returns the value from the hashCode() method.

Let’s modify our transferFunds() method and introduce lock ordering with System.identityHashCode:

public static void transferFunds(final Account fromAccount, final Account toAccount, final BigDecimal amount) {
    int fromHash = System.identityHashCode(fromAccount);
    int toHash = System.identityHashCode(toAccount);

    if (fromHash < toHash) {
        synchronized (fromAccount) {
            System.out.println(Thread.currentThread().getName() + " acquired lock on " + fromAccount);
            synchronized (toAccount) {
                transfer(fromAccount, toAccount, amount);
            }
        }
    } else if (fromHash > toHash) {
        synchronized (toAccount) {
            System.out.println(Thread.currentThread().getName() + " acquired lock on " + toAccount);
            synchronized (fromAccount) {
                transfer(fromAccount, toAccount, amount);
            }
        }
    } else {
        synchronized (sameHashCodeLock) {
            synchronized (fromAccount) {
                System.out.println(Thread.currentThread().getName() + " acquired lock on " + fromAccount);
                synchronized (toAccount) {
                    transfer(fromAccount, toAccount, amount);
                }
            }
        }
    }
}
        

In the code example above, we calculated the hash code for fromAccount and toAccount and defined the lock order depending on the given values.

Since two objects can have the same hash code, we needed to add additional logic and introduce a third sameHashCodeLock lock:

private static final Object sameHashCodeLock = new Object();        

In the else statement, we first acquired a lock on the sameHashCodeLock, ensuring that only one thread obtained locks on the Account objects at a time. This eliminated the possibility of deadlocks.

Avoiding Deadlocks

Going further, let’s discuss how to avoid deadlocks. We should remember that if our program never acquires more than one lock at a time, it can never experience lock-ordering deadlock.?

Timed Lock Attempts

One way our system can recover from deadlock is by using timed lock attempts. We can use the tryLock() method from the Lock interface. Within the method, we can set the timeout, after which the method returns failure if it can’t acquire the lock. This way, a thread will not block indefinitely:

while (true) {
    if (fromAccount.lock.tryLock(1, SECONDS)) {
        System.out.println(Thread.currentThread().getName() + " acquired lock on " + fromAccount);
        try {
            if (toAccount.lock.tryLock(1, SECONDS)) {
                try {
                    transfer(fromAccount, toAccount, amount);
                } finally {
                    toAccount.lock.unlock();
                }
            }
        } finally {
            fromAccount.lock.unlock();
        }
    }

    SECONDS.sleep(10);
}
        

We shouldn’t forget to call the unlock() method in the finally block.

Detecting Deadlock with Thread Dumps

Lastly, let’s see how to detect deadlock using thread dumps and the fastThread tool. A thread dump consists of a stack trace for each running thread and locking information.?

A portion of the generated thread dump causing a deadlock looks like the following:

"Thread-0":
  waiting to lock monitor 0x000060000085c340 (object 0x000000070f994f08, a com.tier1app.deadlock.Account),
  which is held by "Thread-1"

"Thread-1":
  waiting to lock monitor 0x0000600000850410 (object 0x000000070f991c90, a com.tier1app.deadlock.Account),
  which is held by "Thread-0"        

To check whether our application is suffering from the deadlock, we can upload the thread dump in the fastThread tool:


The entire report can be found here.

Next, we can see the details causing it:


Conclusion

In this article, we learned what a deadlock is, how to fix it, and how to avoid it.

To summarize, a deadlock occurs in concurrent applications when threads block each other while waiting for a resource acquired from another thread to become available. One way to fix a deadlock is to define lock ordering using the object’s hash code.

Finally, we can detect deadlock using thread dump and the fastThread tool.

要查看或添加评论,请登录

yCrash的更多文章

社区洞察

其他会员也浏览了