Java Concurrency and Multithreading

Reentrant Locks: Avoiding Deadlocks with Nested Method Calls

Here’s an example scenario where a reentrant lock is useful, but a synchronized lock would not work correctly:

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

public class BankAccount {
    private double balance;
    private final ReentrantLock lock = new ReentrantLock();

    public void deposit(double amount) {
        lock.lock(); // Acquire the lock
        try {
            double newBalance = balance + amount;
            updateBalance(newBalance); // Calls another method while holding the lock
        } finally {
            lock.unlock(); // Release the lock
        }
    }

    public void withdraw(double amount) {
        lock.lock(); // Acquire the lock
        try {
            double newBalance = balance - amount;
            updateBalance(newBalance); // Calls another method while holding the lock
        } finally {
            lock.unlock(); // Release the lock
        }
    }

    private void updateBalance(double newBalance) {
        lock.lock(); // Reacquire the lock (reentrancy)
        try {
            // Perform some additional operations on the balance
            balance = newBalance;
        } finally {
            lock.unlock(); // Release the lock
        }
    }

    // Other methods...
}

In this example, the deposit and withdraw methods acquire a reentrant lock before modifying the account balance. Inside these methods, they call the updateBalance method, which also needs to access the shared balance variable.

If we were using a synchronized lock instead of a reentrant lock, the updateBalance method would block when trying to acquire the lock that is already held by the calling thread (deposit or withdraw), causing a deadlock.

However, with a reentrant lock, the same thread can acquire the lock multiple times without causing a deadlock. When updateBalance is called from deposit or withdraw, the thread can reacquire the lock it already holds, increment an entry count, and proceed with updating the balance.

After the balance is updated, the thread releases the lock, decrementing the entry count. When the entry count reaches zero, the lock is released for other threads to acquire.

Using a synchronized lock in this scenario would cause a deadlock because the thread would be blocked when trying to acquire the lock it already owns. Reentrant locks solve this issue by allowing a thread to reacquire a lock it already holds, making them suitable for situations where a method needs to call another method that also requires access to the same shared resource.

The Java synchronized keyword does not provide a way to set a timeout for acquiring a lock, which can lead to a scenario where a thread might end up waiting indefinitely for the lock. This can cause performance issues, deadlocks, or other problems in your application.

The ReentrantLock class from the Java Lock API provides more flexibility and control over locking behavior. It includes the tryLock() method, which allows you to attempt to acquire the lock and return immediately if the lock is not available, without waiting indefinitely.

Here’s an example scenario where a thread might end up waiting indefinitely when using the synchronized keyword, and how it can be solved using a ReentrantLock:

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

class SynchronizedExample {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(SynchronizedExample::synchronizedMethod);
        Thread thread2 = new Thread(SynchronizedExample::synchronizedMethod);

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

    private static void synchronizedMethod() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " acquired the lock.");
            try {
                TimeUnit.SECONDS.sleep(5); // Simulate some work
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

In this example, two threads (thread1 and thread2) are created and started. Both threads attempt to acquire the same lock (lock object) by calling the synchronizedMethod(). However, since the method is synchronized, only one thread can hold the lock at a time.

If thread1 acquires the lock first, thread2 will be blocked and wait indefinitely until thread1 releases the lock. This can lead to performance issues or deadlocks if the lock is not released due to an error or other circumstances.

To solve this problem using a ReentrantLock, you can use the tryLock() method to acquire the lock with a timeout. If the lock cannot be acquired within the specified timeout, the thread can take alternative actions instead of waiting indefinitely. Here’s the modified example using a ReentrantLock:

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

class ReentrantLockExample {
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread thread1 = new Thread(ReentrantLockExample::lockMethod);
        Thread thread2 = new Thread(ReentrantLockExample::lockMethod);

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

    private static void lockMethod() {
        try {
            if (lock.tryLock(1, TimeUnit.SECONDS)) {
                try {
                    System.out.println(Thread.currentThread().getName() + " acquired the lock.");
                    TimeUnit.SECONDS.sleep(5); // Simulate some work
                } finally {
                    lock.unlock();
                }
            } else {
                System.out.println(Thread.currentThread().getName() + " could not acquire the lock.");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

In this modified example, we use a ReentrantLock instead of the synchronized keyword. The lockMethod() attempts to acquire the lock using lock.tryLock(1, TimeUnit.SECONDS), which tries to acquire the lock and waits for up to 1 second. If the lock is acquired within the timeout, the thread proceeds to execute the critical section and releases the lock in the finally block.

However, if the lock cannot be acquired within the timeout, the tryLock() method returns false, and the thread can take alternative actions. In this case, it prints a message indicating that it could not acquire the lock.

By using the tryLock() method, you can prevent threads from waiting indefinitely for a lock, which can help improve the performance and reliability of your application.

Avatar

Neelabh

About Author

As Neelabh Singh, I am a Senior Software Engineer with 6.6 years of experience, specializing in Java technologies, Microservices, AWS, Algorithms, and Data Structures. I am also a technology blogger and an active participant in several online coding communities.

You may also like

Blog Java Java 8 Features

Avoid NullPointerException in Java with Optional: A Comprehensive Guide

Are you tired of dealing with avoid NullPointerExceptions(NPEs) in your Java code? Look no further than the Optional class introduced
Blog Java Java 8 Features

Unlocking the Power of Java 8 Streams: A Guide to Efficient Data Processing

What are Java 8 Streams? At its core, a stream in Java 8 is a sequence of elements that facilitates