Locks & Conditions
Locks & Conditions
The synchronized keyword has served Java developers since 1995, but it has real limitations: you cannot time out while waiting for a lock, you cannot interrupt a waiting thread, and a single monitor object supports only one wait-set. The java.util.concurrent.locks package, introduced in Java 5, solves all of these problems by exposing locking as an explicit object you can program against.
Why Not Just Use synchronized?
Before looking at the new APIs, it helps to be precise about what synchronized cannot do:
- No timed acquisition — a thread that calls a synchronized method blocks indefinitely.
- No interruptible lock waits — you cannot cancel a thread that is waiting to acquire a monitor.
- One condition per lock —
wait()/notifyAll()are tied to the object itself; you cannot have separate "not full" and "not empty" wait-sets on the same lock. - No read/write distinction — every access is exclusive, even pure reads.
ReentrantLock — The Direct Replacement
ReentrantLock behaves like synchronized but gives you programmatic control. The most important rule: always release the lock in a finally block so it is released even if an exception is thrown.
lock.lock() and the try body, the lock is never released and every other thread blocks forever. Always pair lock.lock() with try { ... } finally { lock.unlock(); }.
Timed and interruptible acquisition are where ReentrantLock clearly outperforms synchronized:
The fairness flag is another lever: new ReentrantLock(true) grants the lock to the longest-waiting thread. This prevents starvation but reduces throughput — threads queue up instead of competing.
ReadWriteLock — Separating Reads from Writes
Many data structures are read far more often than they are written. ReentrantReadWriteLock maintains two locks from a single object: a read lock that multiple threads can hold simultaneously, and a write lock that is exclusive.
ReentrantLock or even synchronized can be faster because ReadWriteLock has more internal bookkeeping.
StampedLock — The Modern Alternative
Java 8 introduced StampedLock, which adds an optimistic read mode. An optimistic read does not block writers at all — it reads without acquiring any lock, then validates a stamp to check whether a write happened during the read. Only if validation fails does it fall back to a full read lock.
Conditions — Multiple Wait-Sets on One Lock
A Condition is what you get instead of wait()/notify() when you use explicit locks. You create one per logical predicate; a single lock can have many conditions, which eliminates spurious wakeups of threads that were not waiting for the predicate that just became true.
Condition.await(), spurious wakeups are possible. The canonical pattern is while (!condition) { await(); }, never if (!condition) { await(); }.
Choosing the Right Tool
- synchronized — simple critical sections with no timeout, cancellation, or multiple conditions needed. Prefer it for its clarity and JVM optimizations (biased locking, lock elision).
- ReentrantLock — when you need timed or interruptible lock acquisition, or multiple
Conditionobjects on the same lock. - ReadWriteLock — read-heavy workloads with infrequent writes and meaningful work inside the lock.
- StampedLock — maximum throughput on read-dominated data, when you are comfortable with the more complex API and the fact that it is not reentrant.
Summary
ReentrantLock gives you everything synchronized does plus timed acquisition, interruptibility, and multiple Condition objects. ReadWriteLock boosts concurrency when reads dominate. StampedLock pushes further with optimistic reads. Use explicit locks only when the simpler tools are not sufficient — the added flexibility comes with added responsibility to release locks correctly.