Locks — ReentrantLock, ReadWriteLock, StampedLock, and Condition
In this tutorial, you will learn about Locks. We cover key concepts, practical examples, and best practices to help you master this topic.
Java's java.util.concurrent.locks package provides advanced locking mechanisms beyond synchronized, including ReentrantLock, ReadWriteLock, StampedLock, and Condition for flexible thread coordination. While synchronized is simple and sufficient for most cases, explicit locks offer try-lock, timed lock, interruptible lock, and multiple condition queues.
What You'll Learn
- ReentrantLock: explicit lock with features
- ReadWriteLock: multiple readers, single writer
- StampedLock: optimistic read locking
- Condition: signal/wait alternative to Object methods
Why It Matters
ReadWriteLock dramatically improves throughput for read-heavy workloads where readers never conflict with readers. StampedLock supports optimistic reads that avoid locking entirely when there is no contention.
Real-World Use
Caches use ReadWriteLock — many threads read, few write. Transaction logs use ReentrantLock with fairness to prevent starvation. StampedLock is used in high-throughput data structures.
ReentrantLock
import java.util.concurrent.locks.*;
class Counter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
Always release the lock in finally to prevent lock leaks.
tryLock
if (lock.tryLock()) {
try {
// critical section
} finally {
lock.unlock();
}
} else {
// alternative action
}
// With timeout
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) { ... }
Fairness
// Fair lock — longest-waiting thread acquires first (slower)
ReentrantLock fairLock = new ReentrantLock(true);
Interruptible Lock
lock.lockInterruptibly(); // throws InterruptedException if thread is interrupted
ReadWriteLock
Allows multiple readers OR a single writer — never both:
class ReadWriteMap<K, V> {
private final Map<K, V> map = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
public V get(K key) {
rwLock.readLock().lock();
try {
return map.get(key);
} finally {
rwLock.readLock().unlock();
}
}
public void put(K key, V value) {
rwLock.writeLock().lock();
try {
map.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
}
- Multiple threads can read concurrently — high throughput
- Writing is exclusive — blocks all readers and other writers
- Ideal for caches, configuration stores, lookup tables
StampedLock
StampedLock provides three modes: read, write, and optimistic read:
class Point {
private double x, y;
private final StampedLock lock = new StampedLock();
public void move(double dx, double dy) {
long stamp = lock.writeLock();
try {
x += dx;
y += dy;
} finally {
lock.unlockWrite(stamp);
}
}
public double distanceFromOrigin() {
long stamp = lock.tryOptimisticRead();
double currentX = x;
double currentY = y;
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
currentX = x;
currentY = y;
} finally {
lock.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
Optimistic read does not block — it reads optimistically and validates afterward. If a write occurred during the read, it falls back to a full read lock. This can be much faster in low-contention scenarios.
Condition
Condition provides await/signal instead of wait/notify, with the advantage of multiple condition queues per lock:
class BoundedBuffer<T> {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final List<T> buffer = new LinkedList<>();
private final int capacity;
public BoundedBuffer(int capacity) {
this.capacity = capacity;
}
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (buffer.size() == capacity) {
notFull.await(); // wait until not full
}
buffer.add(item);
notEmpty.signal(); // signal consumers
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (buffer.isEmpty()) {
notEmpty.await(); // wait until not empty
}
T item = buffer.remove(0);
notFull.signal(); // signal producers
return item;
} finally {
lock.unlock();
}
}
}
Lock vs Synchronized
| Feature | synchronized | Lock |
|---|---|---|
| Simplicity | Simple, implicit | More verbose |
| Try-lock | No | Yes |
| Timed acquisition | No | Yes |
| Interruptible | No | Yes |
| Fairness | No (unfair) | Configurable |
| Multiple conditions | Single wait set | Multiple Conditions |
| Performance (modern JVM) | Excellent | Comparable |
Common Mistakes
- Forgetting to unlock in finally. If an exception occurs between lock and unlock, the lock is never released — causing Deadlock.
- Using ReentrantLock when synchronized suffices. If you do not need try-lock, timed lock, or fairness,
synchronizedis simpler. - Downgrading from write lock to read lock. You can release the write lock and acquire the read lock, but ReentrantReadWriteLock does not support direct downgrade.
- Calling
await()without checking the condition. Always usewhile (condition)notif (condition)— spurious wakeups can occur. - Using StampedLock optimism incorrectly. Validate the stamp — if validation fails, the read may be inconsistent.
Practice Questions
1. What is the advantage of ReadWriteLock over ReentrantLock?
It allows multiple concurrent readers when no writer holds the lock, significantly improving throughput for read-heavy workloads.
2. What is an optimistic read in StampedLock?
A read that does not block — it reads the data and then validates that no write occurred concurrently. If a write occurred, it retries with a full read lock.
3. What is the difference between signal() and signalAll()?
signal() wakes up one waiting thread. signalAll() wakes up all waiting threads. Use signalAll() when threads may be waiting for different conditions.
4. How does a Condition differ from Object.wait/notify?
Condition allows multiple wait queues per lock, while Object's methods use a single wait set. Condition also supports fairness and interruptible waits.
5. What is a try-lock?
lock.tryLock() attempts to acquire the lock without blocking — returns true if acquired, false otherwise. Useful for avoiding deadlocks by backing off.
Challenge Question:
Implement a thread-safe ConcurrentCache<K, V> using ReadWriteLock. The cache supports get, put, invalidate, and clear. Use ConcurrentHashMap internally but protect bulk operations with the write lock. Also implement a getOrCompute(K key, Function<K, V> loader) that loads the value on cache miss.
FAQ
Mini Project
Write a program LockDemo.java that:
- Implements a thread-safe inventory counter using ReentrantLock
- Implements a configuration cache using ReadWriteLock — many readers, occasional writes
- Implements a 3D point class using StampedLock with optimistic reads
- Implements a bounded buffer using Condition (await/signal)
- Measures throughput: synchronized vs ReentrantLock vs ReadWriteLock vs StampedLock
- Demonstrates tryLock to avoid deadlock with a lock-ordering example
- Shows the difference between fair and unfair locks with timing
What's Next
Creating threads manually is tedious and error-prone. Lesson 54 covers executors — thread pools, ScheduledExecutorService, invokeAll, invokeAny, and the ExecutorService framework for managing thread lifecycles.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro