ReentrantLock and Conditions
Use ReentrantLock and Condition for fine-grained thread synchronization beyond synchronized blocks.
Overview
Key Points
- •lock.lock() acquires the lock; lock.unlock() releases it
- •Always call unlock() in a finally block to prevent lock leaks
- •lock.tryLock() returns true if the lock is available, false otherwise (no blocking)
- •lock.tryLock(timeout, unit) attempts to acquire within a time window
- •lock.lockInterruptibly() allows the thread to be interrupted while waiting
- •lock.newCondition() creates a Condition bound to this lock
- •condition.await() releases the lock and waits (like Object.wait)
- •condition.signal() wakes one thread waiting on this condition
- •condition.signalAll() wakes all threads waiting on this condition
- •ReadWriteLock provides separate read and write locks for better read concurrency
Examples
Basic ReentrantLock replacing synchronized
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // always in finally
}
}
public int get() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Counter c = new Counter();
Thread t1 = new Thread(() -> { for (int i = 0; i < 10000; i++) c.increment(); });
Thread t2 = new Thread(() -> { for (int i = 0; i < 10000; i++) c.increment(); });
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(c.get()); // always 20000
}
}The finally block guarantees unlock even if an exception occurs inside the critical section. Forgetting finally is a common source of deadlocks.
Condition variables for producer-consumer coordination
import java.util.*;
import java.util.concurrent.locks.*;
public class BoundedQueue<T> {
private final Queue<T> queue = new LinkedList<>();
private final int maxSize;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
BoundedQueue(int maxSize) { this.maxSize = maxSize; }
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == maxSize) notFull.await();
queue.add(item);
notEmpty.signal();
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) notEmpty.await();
T item = queue.poll();
notFull.signal();
return item;
} finally {
lock.unlock();
}
}
}Two separate conditions (notFull, notEmpty) mean put only wakes threads blocked on take, and vice versa. With a single Object.wait/notifyAll, all threads would be woken even when the wake-up does not apply to them.
tryLock for non-blocking attempts
import java.util.concurrent.locks.ReentrantLock;
public class Main {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Runnable task = () -> {
if (lock.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " acquired lock");
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " could not acquire lock, skipping");
}
};
new Thread(task, "t1").start();
new Thread(task, "t2").start();
}
}tryLock() returns immediately. One thread will log "acquired" and the other will log "could not acquire". Use tryLock(timeout, unit) to wait up to a time limit before giving up.
Common Mistakes
- Not calling unlock() in a finally block. If the critical section throws, the lock stays acquired forever, deadlocking any other thread that tries to acquire it.
- Using lock.notify() or lock.wait(). These methods belong to Object and cannot be used with ReentrantLock. Use Condition.signal() and Condition.await() instead.
- Checking condition with if instead of while before await(). Spurious wakeups can occur; always recheck the condition in a while loop.
- Acquiring the lock in one method and forgetting to release it in another code path. Because tryLock can return false, callers sometimes forget the early-return path skips unlock.
Note
Use ReentrantReadWriteLock when reads are frequent and writes are rare. Multiple threads can hold the read lock simultaneously, but a write lock is exclusive. This can dramatically improve throughput for read-heavy data structures.
Exercises
Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.
Completed: 0 / 2 exercises
Knowledge Check
Question 1 of 4