Java ReentrantLock and Condition: Advanced Locking
The synchronized keyword is simple but limited: you cannot try a lock with a timeout, implement fairness, or have multiple wait sets. ReentrantLock provides all of these. Condition variables (created from a lock) let threads wait for specific state changes more precisely than Object.wait()/notify(). ReentrantReadWriteLock allows multiple concurrent readers but exclusive writers: ideal for read-heavy shared data.
Contents
ReentrantLock: Explicit Locking
ReentrantLocksynchronizedfinallylock()tryLock()
import java.util.concurrent.locks.*;
// ReentrantLock — same semantics as synchronized, but explicit lock/unlock
ReentrantLock lock = new ReentrantLock();
// ALWAYS use try-finally to guarantee unlock even if an exception is thrown
lock.lock();
try {
// critical section
doWork();
} finally {
lock.unlock(); // MUST be in finally
}
// "Reentrant" — the same thread can acquire the lock multiple times
lock.lock();
try {
lock.lock(); // same thread — succeeds, hold count goes to 2
try {
doMoreWork();
} finally {
lock.unlock(); // hold count goes back to 1
}
} finally {
lock.unlock(); // hold count goes to 0 — actually released
}
// Fairness — new ReentrantLock(true) grants locks in arrival order
// Fair locks prevent thread starvation but reduce throughput
ReentrantLock fairLock = new ReentrantLock(true);
// Lock introspection
System.out.println(lock.isLocked()); // true if any thread holds it
System.out.println(lock.isHeldByCurrentThread()); // true if THIS thread holds it
System.out.println(lock.getHoldCount()); // re-entrant hold count
System.out.println(lock.getQueueLength()); // threads waiting to acquire
tryLock: Non-blocking and Timed Attempts
tryLock()falsetryLock(timeout, unit)
ReentrantLock lock = new ReentrantLock();
// tryLock() — returns immediately: true if acquired, false if not
if (lock.tryLock()) {
try {
doWork();
} finally {
lock.unlock();
}
} else {
System.out.println("Could not acquire lock — skipping");
}
// tryLock(timeout, unit) — wait up to timeout
try {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
doWork();
} finally {
lock.unlock();
}
} else {
System.out.println("Gave up waiting for lock");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// lockInterruptibly() — like lock() but can be interrupted while waiting
try {
lock.lockInterruptibly();
try {
doWork();
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // thread was interrupted while waiting
}
// Deadlock avoidance using tryLock — classic two-lock transfer
void transferMoney(Account from, Account to, int amount) {
while (true) {
if (from.lock.tryLock()) {
try {
if (to.lock.tryLock()) {
try {
from.debit(amount);
to.credit(amount);
return;
} finally { to.lock.unlock(); }
}
} finally { from.lock.unlock(); }
}
Thread.yield(); // back off before retry
}
}
tryLock()Condition Variables: await and signal
A Conditionlock.newCondition()Object.wait()/notify()await()signal()signalAll()whileif
// Condition — created from a Lock; replaces Object.wait()/notify()
// Each lock can have multiple Conditions — more precise than Object.notify()
ReentrantLock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition(); // "not empty" condition
Condition notFull = lock.newCondition(); // "not full" condition
// Classic bounded buffer (producer-consumer) using Condition
class BoundedBuffer<T> {
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Object[] items;
private int head, tail, count;
BoundedBuffer(int capacity) { items = new Object[capacity]; }
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await(); // wait until not full; releases lock while waiting
}
items[tail] = item;
tail = (tail + 1) % items.length;
count++;
notEmpty.signal(); // wake one thread waiting for "not empty"
} finally {
lock.unlock();
}
}
@SuppressWarnings("unchecked")
public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // wait until not empty
}
T item = (T) items[head];
head = (head + 1) % items.length;
count--;
notFull.signal(); // wake one thread waiting for "not full"
return item;
} finally {
lock.unlock();
}
}
}
// Condition methods:
// await() — release lock, wait until signalled or interrupted
// await(time, unit) — wait with timeout
// awaitUninterruptibly()— ignore interrupts while waiting
// signal() — wake ONE thread waiting on this condition
// signalAll() — wake ALL threads waiting on this condition
// Always check condition in a WHILE loop (not if) — handle spurious wakeups
whileifReentrantReadWriteLock
ReentrantReadWriteLockreadLock()writeLock()
// ReentrantReadWriteLock — allows many readers OR one writer
// Read lock: shared — multiple threads can hold simultaneously
// Write lock: exclusive — only one thread; blocks all readers and writers
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
Map<String, String> cache = new HashMap<>();
// Read — many threads can read concurrently
public String get(String key) {
readLock.lock();
try {
return cache.get(key);
} finally {
readLock.unlock();
}
}
// Write — exclusive access
public void put(String key, String value) {
writeLock.lock();
try {
cache.put(key, value);
} finally {
writeLock.unlock();
}
}
// Lock downgrading — hold write lock, acquire read lock, release write lock
// Ensures no other writer can sneak in between write and read
public void updateAndRead(String key, String value) {
writeLock.lock();
try {
cache.put(key, value);
readLock.lock(); // acquire read lock while holding write lock
} finally {
writeLock.unlock(); // release write lock — now holding only read lock
}
try {
String result = cache.get(key); // safe read — no writer can intervene
System.out.println("Updated: " + result);
} finally {
readLock.unlock();
}
}
// Note: lock UPGRADING (read → write) is NOT supported — would deadlock
synchronized vs ReentrantLock
synchronizedReentrantLocksynchronizedtryLock()lockInterruptibly()Condition
// synchronized — simpler, less verbose, JVM can optimize
// ReentrantLock — more features, same or slightly worse perf in low-contention
// Use synchronized when:
synchronized (this) {
// - Simple mutual exclusion
// - No timeout needed
// - No multiple wait conditions
}
// Use ReentrantLock when you need:
// - tryLock() — non-blocking attempt
// - tryLock(timeout) — bounded waiting
// - lockInterruptibly() — cancellable wait
// - Multiple Condition objects
// - Fairness control
// - Lock introspection (isLocked, getQueueLength, etc.)
// Performance note (Java 21+):
// With virtual threads, lightweight blocking is cheap
// The advantage of tryLock/timed locks is less important for I/O-bound code
| Feature | synchronized | ReentrantLock |
|---|---|---|
| Automatic unlock | Yes (JVM) | No (finally) |
| Interruptible wait | No | Yes |
| Timed tryLock | No | Yes |
| Multiple conditions | One (obj) | Many |
| Fairness option | No | Yes |
| Reentrancy | Yes | Yes |
| Condition#await/signal | Object.wait | Condition.* |