Contents
- ReentrantLock — Explicit Locking
- tryLock — Non-blocking and Timed Attempts
- Condition Variables — await and signal
- ReentrantReadWriteLock
- synchronized vs ReentrantLock
ReentrantLock is reentrant, meaning the same thread can acquire it multiple times without deadlocking — each acquisition increments an internal hold count that must be matched by an equal number of unlocks. Unlike synchronized, the lock is NOT released automatically; it must always be released in a finally block to guarantee unlock even when an exception is thrown. lock() blocks indefinitely until the lock is available, while tryLock() returns immediately with a boolean result.
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() returns false immediately if the lock is held by another thread, so the calling thread is never blocked — it can take an alternative action instead. tryLock(timeout, unit) waits up to the specified duration before giving up. Both forms help avoid deadlocks and priority inversion by ensuring threads do not wait indefinitely for a lock they may never get.
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() does NOT respect fairness even on a fair lock — it acquires immediately if available, bypassing the wait queue. This is by design for cases where you want a best-effort attempt without queuing.
A Condition is obtained by calling lock.newCondition(); a single lock can have multiple independent conditions, which is a key advantage over Object.wait()/notify(). Calling await() atomically releases the lock and suspends the thread until another thread calls signal() (wakes one waiting thread) or signalAll() (wakes all). Because spurious wakeups are permitted by the Java specification, the wait condition must always be checked inside a while loop, never an if.
// 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
Always use a while loop, not an if, when checking wait conditions. Spurious wakeups (where a thread wakes without being signalled) are permitted by the Java specification and do occur in practice.
ReentrantReadWriteLock allows multiple threads to hold the read lock simultaneously, but the write lock is exclusive — a writer blocks all readers and other writers. Use readLock() for shared reads and writeLock() for exclusive writes. This separation significantly improves throughput for read-heavy workloads where reads vastly outnumber writes, because concurrent readers do not block each other.
// 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 is simpler, requires no explicit unlock, and is sufficient for the majority of use cases. Prefer ReentrantLock when you need capabilities that synchronized cannot provide: non-blocking tryLock(), timed lock acquisition, interruptible waiting with lockInterruptibly(), multiple Condition objects on a single lock, or a fairness policy that prevents thread starvation.
// 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
// Quick comparison:
// | 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.* |