How to Avoid Deadlocks in Java

In the previous article, we talked about how deadlocks occur in Java while working in a multi-threaded environment. Now, we will see how to prevent deadlocks by explaining a couple of corner cases.

Avoid Nested Locks

In the previous article, we showed an example for deadlock, and it contains nested locks as follows.

...
Thread thread1 = new Thread(() -> {
    synchronized (stockCounter1) {
        log.info("Locked stockCounter1");
        try {
            Thread.sleep(400);
        }
        catch (InterruptedException e) {
            log.error("interrupted", e);
        }
        synchronized (stockCounter2) {
            log.info("Locked stockCounter2");
        }
    }
});
...

If there are multiple threads locking stockCounter resources, most probably you will have a deadlock. What if we use a sequential lock instead of using a nested one? Let’s take a look at the following scenario before jumping into code examples

  • Let’s say that we have an e-commerce module where you can see order and payment-related operations.
  • Thread-1 creates orders and charges the customer for the order’s total cost.
  • Thread-2 charges a customer and updates the order status.

To simulate deadlock, we would have something as follows.

...
synchronized (orderService) {
    synchronized (paymentService) {
        log.info("Locked paymentService");
    }
}
...
synchronized (paymentService) {
    synchronized (orderService) {
        log.info("Locked orderService");
    }
}
...

Instead of having nested locks, what if we convert them to the sequential locks as follows?

package com.huseyin.deadlockprevention;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SequentialLock {

    public static void main(String[] args) throws InterruptedException {
        PaymentService paymentService = new PaymentService();
        OrderService orderService = new OrderService();

        Thread thread1 = new Thread(() -> {
            synchronized (orderService) {
                log.info("Locked orderService");
                orderService.create(12);
                try {
                    Thread.sleep(400);
                }
                catch (InterruptedException e) {
                    log.error("interrupted", e);
                }

            }
            synchronized (paymentService) {
                log.info("Locked paymentService");
                paymentService.pay(3);
                try {
                    Thread.sleep(400);
                }
                catch (InterruptedException e) {
                    log.error("interrupted", e);
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (paymentService) {
                log.info("Locked paymentService");
                paymentService.pay(4);
                try {
                    Thread.sleep(400);
                }
                catch (InterruptedException e) {
                    log.error("interrupted", e);
                }
            }
            synchronized (orderService) {
                log.info("Locked orderService");
                orderService.update(1, "SUCCESS");
                try {
                    Thread.sleep(400);
                }
                catch (InterruptedException e) {
                    log.error("interrupted", e);
                }
            }
        });
        thread1.start();
        thread2.start();
    }
}

Notice that, since this is a sequential lock, if you have a problem with the second lock, you may need to revert operation in the first synchronization block. You may not want to accept sequential locks for all the business use-cases, and this might force you to stay on a nested lock strategy. This time you can use a lock, but with a timeout. Let’s take a look at how we can simulate it with the same use case.

You can try the above example with the following command after you clone the repository here

./gradlew :deadlockprevention:run \
    -PmainClass=com.huseyin.deadlockprevention.SequentialLock

Use Thread Join to Wait Until it Finishes

In this practice, even if we use nested synchronized blocks, we will wait for the first thread to be finished before starting the second one. Notice the join() method at the end of example.

package com.huseyin.deadlockprevention;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class NestedLockWithWaitTime {

    public static void main(String[] args) throws InterruptedException {
        PaymentService paymentService = new PaymentService();
        OrderService orderService = new OrderService();

        Thread thread1 = new Thread(() -> {
            synchronized (orderService) {
                log.info("Locked orderService");
                orderService.create(12);
                try {
                    Thread.sleep(400);
                }
                catch (InterruptedException e) {
                    log.error("interrupted", e);
                }
                synchronized (paymentService) {
                    log.info("Locked paymentService");
                    paymentService.pay(4);
                    try {
                        Thread.sleep(400);
                    }
                    catch (InterruptedException e) {
                        log.error("interrupted", e);
                    }
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (paymentService) {
                log.info("Locked orderService");
                paymentService.pay(4);
                try {
                    Thread.sleep(400);
                }
                catch (InterruptedException e) {
                    log.error("interrupted", e);
                }
                synchronized (orderService) {
                    log.info("Locked orderService");
                    orderService.update(1, "SUCCESS");
                    try {
                        Thread.sleep(400);
                    }
                    catch (InterruptedException e) {
                        log.error("interrupted", e);
                    }
                }
            }

        });
        thread1.start();
        thread1.join();
        thread2.start();
        thread2.join();
    }
}

After visiting some corner cases about lock mechanisms, let’s continue with different prevention mechanisms. You can try example with following command

./gradlew :deadlockprevention:run \
    -PmainClass=com.huseyin.deadlockprevention.NestedLockWithJoin

Avoid Unnecessary Locks

If it is ok for you to not use locks, just don’t use them 🙂 Using locks is reasonable for data-critic operations like maintaining stock count, customer balance, etc. If you don’t have such a use case, you can just avoid using it, and you can do extra checks only before persisting your data.

As always, you can see the code examples here

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s