Understanding and Solving Deadlocks in Java

Understanding and Solving Deadlocks in Java are one of the most common problems in concurrent programming. They occur when multiple threads are waiting indefinitely for resources held by each other. In Java, this can happen with both synchronized blocks and ReentrantLock.

In this article, we will explore examples of deadlocks and their solutions in detail.

Deadlock Problem Using Synchronized

In this first example, which demonstrates a deadlock using synchronized, imagine a scenario where two threads transfer money between two accounts in opposite directions. Below is a simplified version of the code that can lead to a deadlock:

public static void transfer(Account from, Account to, int amount) {
    synchronized (from) {
        // Simulate delay
        Thread.sleep(100);

        synchronized (to) {
            from.withdraw(amount);
            to.deposit(amount);
        }
    }
}

Explanation:

  • Thread-1 locks AccountA and waits to lock AccountB.

  • Thread-2 locks AccountB and waits to lock AccountA.

  • Both threads are now blocked indefinitely, creating a deadlock.

Solving Deadlock Problem Using LockOrder

One effective way to prevent deadlocks is to enforce a consistent global lock order. For instance, we can compare account names and always acquire the lock on the account with the "lower" name first. This ensures that all threads follow the same locking sequence, eliminating the possibility of circular waiting.

public static void transfer(Account from, Account to, int amount) {
    Account firstLock, secondLock;

    if (from.getName().compareTo(to.getName()) < 0) {
        firstLock = from;
        secondLock = to;
    } else {
        firstLock = to;
        secondLock = from;
    }

    synchronized (firstLock) {
        synchronized (secondLock) {
            from.withdraw(amount);
            to.deposit(amount);
        }
    }
}

Explanation:

  • Locks are always acquired in the same order.

  • Circular waiting is prevented.

  • Deadlock cannot occur with this strategy.

Deadlock Problem Using ReentrantLock

In this example, we can observe a deadlock occurring with ReentrantLock. Even when using ReentrantLock, deadlocks can happen if lock() is called directly without a proper lock ordering or a timeout:

from.getLock().lock();
try {
    Thread.sleep(100);
    to.getLock().lock();
    try {
        from.withdraw(amount);
        to.deposit(amount);
    } finally {
        to.getLock().unlock();
    }
} finally {
    from.getLock().unlock();
}

Explanation:

  • Thread-1 locks AccountA and waits for AccountB.

  • Thread-2 locks AccountB and waits for AccountA.

  • Both threads block indefinitely, causing deadlock.

Solving Deadlock Problem Using ReentrantLock

Solving Deadlock with ReentrantLock and tryLock() We can solve this by using tryLock(timeout) with a retry loop. If a lock cannot be acquired, the thread releases any held locks and retries.

while (true) {
    if (from.getLock().tryLock(100, TimeUnit.MILLISECONDS)) {
        try {
            if (to.getLock().tryLock(100, TimeUnit.MILLISECONDS)) {
                try {
                    from.withdraw(amount);
                    to.deposit(amount);
                    break; // transfer successful
                } finally {
                    to.getLock().unlock();
                }
            }
        } finally {
            from.getLock().unlock();
        }
    }
    Thread.sleep(50); // backoff before retry
}

Explanation:

  • The thread attempts to acquire both locks within a timeout.

  • If unsuccessful, it releases any locks held and retries.

  • This prevents indefinite waiting and ensures progress.

  • Deadlock is eliminated because no thread waits forever. Conclusion Deadlocks can silently freeze concurrent applications, but they can be prevented with proper techniques:

  • With synchronized, enforce a consistent lock ordering.

  • With ReentrantLock, use tryLock() with timeouts and retries. Understanding and applying these techniques is essential for building robust multithreaded Java applications.

Conclusion

Deadlocks are a common challenge in concurrent programming that can silently freeze applications if not handled properly. Through the examples presented, we have seen how both synchronized and ReentrantLock can lead to deadlocks when locks are acquired inconsistently or without proper management.

The key takeaways are:

  • With synchronized, enforcing a consistent global lock order ensures that threads always acquire locks in the same sequence, preventing circular waiting.

  • With ReentrantLock, using tryLock() with timeouts and retry logic prevents threads from waiting indefinitely and guarantees progress.

By understanding these patterns and applying these strategies, developers can build robust, deadlock-free multithreaded Java applications, ensuring reliable and efficient concurrent execution.

You can check this code on github

0
Subscribe to my newsletter

Read articles from Luis Gustavo Souza directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Luis Gustavo Souza
Luis Gustavo Souza