Here’s an example scenario where a reentrant lock is useful, but a synchronized lock would not work correctly:
Imagine you have a class called BankAccount
that handles banking operations like depositing and withdrawing money. The class needs to be thread-safe to ensure that multiple threads can perform operations on the same account without causing data inconsistencies.
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.