In Java, managing concurrent access to shared resources is crucial to ensure data integrity and prevent issues like
race conditions. The java.util.concurrent.locks
package provides various locking mechanisms to achieve this,
and one of the most commonly used is the ReentrantLock
.
What is ReentrantLock?
ReentrantLock
is a synchronization primitive that offers more flexibility and features compared to the
traditional synchronized blocks. It is part of the java.util.concurrent.locks
package and
provides a more extensive API for locking and unlocking operations. As the name suggests,
a reentrant lock allows the thread holding the lock to re-enter the lock multiple times without causing a deadlock.
Key Features of ReentrantLock
- Reentrancy: The lock can be acquired multiple times by the same thread. This means if a thread currently holds the lock and tries to acquire it again, it will succeed without blocking.
- Fairness:
ReentrantLock
can be created as fair or non-fair. A fair lock ensures that the longest-waiting thread gets access to the lock first, while a non-fair lock may permit barging, where threads can cut in line. - Condition Variables:
ReentrantLock
provides condition variables through thenewCondition()
method, allowing threads to wait for specific conditions to be met. - Interruptible Lock Acquisition: Threads can be interrupted while waiting to acquire the lock.
- Try Lock: The
tryLock()
method allows a thread to attempt to acquire the lock without blocking.
Basic Usage of ReentrantLock
Here’s a simple example to illustrate the use of ReentrantLock
:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final Lock lock = new ReentrantLock();
private int counter = 0;
public void increment() {
lock.lock(); // Acquire the lock
try {
counter++;
} finally {
lock.unlock(); // Always release the lock in a finally block
}
}
public int getCounter() {
lock.lock(); // Acquire the lock
try {
return counter;
} finally {
lock.unlock(); // Always release the lock in a finally block
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockExample example = new ReentrantLockExample();
// Create 1000 threads that increment the counter
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(example::increment);
threads[i].start();
}
// Wait for all threads to finish
for (Thread thread : threads) {
thread.join();
}
System.out.println("Final counter value: " + example.getCounter());
}
}
In this example:
- The
ReentrantLock
is used to ensure that the increment andgetCounter
methods are thread-safe. - The
lock.lock()
method acquires the lock, andlock.unlock()
releases it. The unlock call is placed in a finally block to ensure the lock is released even if an exception occurs.
Advanced Features
- Fair Lock: To create a fair ReentrantLock, pass true to the constructor:
This ensures that the longest-waiting thread will get the lock first.
Lock fairLock = new ReentrantLock(true);
- Try Lock: To avoid blocking, use tryLock():
if (lock.tryLock()) { try { // Perform lock-protected operations } finally { lock.unlock(); } } else { // Handle the case where the lock was not acquired }
- Condition Variables: Use condition variables for more complex thread coordination:
Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); public void awaitCondition() throws InterruptedException { lock.lock(); try { condition.await(); // Wait until signaled } finally { lock.unlock(); } } public void signalCondition() { lock.lock(); try { condition.signal(); // Signal one waiting thread } finally { lock.unlock(); } }
When to Use ReentrantLock Over Synchronized
- Fairness: If you need a fair locking mechanism to prevent thread starvation.
- Interruptibility: When you need the ability to interrupt threads waiting for the lock.
- Non-blocking Attempts: If you need to attempt to acquire the lock without blocking.
- Condition Variables: When complex waiting and notification patterns are required.
Summary
ReentrantLock
provides a powerful and flexible mechanism for thread synchronization in Java.
It extends the capabilities of traditional synchronized blocks by offering features
like reentrancy, fairness, interruptible lock acquisition, non-blocking attempts, and condition variables.
Understanding and using ReentrantLock
effectively can lead to more robust and maintainable concurrent applications.