ReentrantLock
class. ReentrantLock
is part of the java.util.concurrent.locks
package. It provides a more flexible alternative to the traditional synchronized
keyword. In this blog, we will take an in - depth look at ReentrantLock
, including its fundamental concepts, usage methods, common practices, and best practices.ReentrantLock
is a mutual exclusion lock with the same basic behavior and semantics as the implicit monitor lock accessed using synchronized
methods and statements, but with extended capabilities. The term “reentrant” means that a thread can acquire the same lock multiple times without blocking itself.
lock()
method and release it using the unlock()
method. This is different from the synchronized
keyword, where the lock is automatically released when the block or method exits.ReentrantLock
can be configured to be fair. A fair lock will grant access to the lock in the order in which threads requested it. By default, it is non - fair, which can provide better performance in most cases.ReentrantLock
uses an AbstractQueuedSynchronizer
(AQS) internally. AQS maintains a queue of threads waiting for the lock. When a thread tries to acquire the lock, it first checks if the lock is available. If it is, the thread acquires the lock and marks itself as the owner. If the lock is already held by another thread, the requesting thread is added to the waiting queue.
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
public class ReentrantLockExample {
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + counter.getCount());
}
}
In this example, we have a Counter
class with an integer count
and a ReentrantLock
. The increment()
and getCount()
methods use the lock to ensure thread - safe access to the count
variable. The lock.lock()
method is used to acquire the lock, and lock.unlock()
is used to release it. The try - finally
block is used to ensure that the lock is always released, even if an exception occurs.
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TimeoutLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void performTask() {
try {
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println("Lock acquired. Performing task...");
Thread.sleep(2000);
} finally {
lock.unlock();
}
} else {
System.out.println("Could not acquire lock within the timeout.");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
TimeoutLockExample example = new TimeoutLockExample();
Thread t1 = new Thread(example::performTask);
Thread t2 = new Thread(example::performTask);
t1.start();
t2.start();
}
}
The tryLock(long timeout, TimeUnit unit)
method attempts to acquire the lock within the specified timeout. If the lock is acquired, it returns true
; otherwise, it returns false
.
import java.util.concurrent.locks.ReentrantLock;
public class InterruptibleLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void performTask() {
try {
lock.lockInterruptibly();
try {
System.out.println("Lock acquired. Performing task...");
Thread.sleep(2000);
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("Thread interrupted while waiting for the lock.");
}
}
public static void main(String[] args) {
InterruptibleLockExample example = new InterruptibleLockExample();
Thread t1 = new Thread(example::performTask);
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.interrupt();
}
}
The lockInterruptibly()
method allows a thread to be interrupted while waiting for the lock. If the thread is interrupted, an InterruptedException
is thrown.
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
class ProducerConsumer {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity = 5;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public void produce() throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await();
}
int item = (int) (Math.random() * 100);
queue.add(item);
System.out.println("Produced: " + item);
notEmpty.signal();
} finally {
lock.unlock();
}
}
public void consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await();
}
int item = queue.poll();
System.out.println("Consumed: " + item);
notFull.signal();
} finally {
lock.unlock();
}
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer();
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
pc.produce();
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
pc.consume();
Thread.sleep(200);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producer.start();
consumer.start();
try {
producer.join();
consumer.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
In this producer - consumer example, ReentrantLock
is used to control access to the shared queue. The Condition
objects notFull
and notEmpty
are used to signal and wait for the appropriate conditions.
As shown in the previous examples, always use a try - finally
block when using ReentrantLock
. This ensures that the lock is released even if an exception occurs during the execution of the critical section.
Fair locks can be useful in scenarios where thread starvation is a concern. However, fair locks generally have lower performance compared to non - fair locks. Use fair locks only when necessary.
Keep the critical section as small as possible. Holding the lock for a long time can reduce the concurrency of the application and increase the chances of deadlocks.
ReentrantLock
is a powerful and flexible tool for thread synchronization in Java. It provides more features than the traditional synchronized
keyword, such as lock timeout, interruptible lock acquisition, and fairness. By understanding its fundamental concepts, usage methods, common practices, and best practices, developers can use ReentrantLock
effectively to build robust and efficient multi - threaded applications.