Why Explicit Locks?
The synchronized keyword handles most locking needs, but it has rigid semantics. The java.util.concurrent.locks package provides explicit lock implementations with richer capabilities:
- Try-lock — attempt to acquire without blocking forever
- Timed lock — give up after a timeout
- Interruptible lock — can be interrupted while waiting
- Fairness — first-come-first-served ordering
- Multiple conditions — separate wait sets for different events
- Non-block-structured — lock and unlock in different scopes
ReentrantLock
The most commonly used explicit lock. It works like synchronized but with explicit lock() and unlock() calls.
Basic Usage
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SafeCounter {
private final Lock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // ALWAYS in finally — guarantees release on exception
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
Why “Reentrant”?
A reentrant lock can be acquired multiple times by the same thread without deadlocking:
lock.lock(); // hold count = 1
lock.lock(); // hold count = 2 (same thread — allowed)
// ... work ...
lock.unlock(); // hold count = 1
lock.unlock(); // hold count = 0 — fully released
This is essential when a synchronized method calls another synchronized method on the same object. Without reentrancy, the thread would deadlock on itself.
Fair vs Unfair Locks
Lock unfairLock = new ReentrantLock(); // Default: unfair
Lock fairLock = new ReentrantLock(true); // Fair: FIFO ordering
| Aspect | Unfair (default) | Fair |
|---|---|---|
| Throughput | Higher | Lower (10-100x slower under contention) |
| Starvation possible? | Yes | No |
| Ordering | No guarantee | FIFO |
| Use when | Performance matters, starvation unlikely | All threads must get equal access |
Fair locks guarantee that the longest-waiting thread gets the lock next, but at a significant performance cost because every acquisition requires checking the queue.
tryLock — Non-Blocking Acquisition
if (lock.tryLock()) {
try {
// Critical section — lock acquired
} finally {
lock.unlock();
}
} else {
// Lock was not available — do fallback logic
System.out.println("Could not acquire lock, skipping operation");
}
tryLock with Timeout
try {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
// Got the lock within 500ms
} finally {
lock.unlock();
}
} else {
// Timed out — lock was not available within 500ms
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
lockInterruptibly — Interruptible Waiting
try {
lock.lockInterruptibly(); // Can be interrupted while waiting
try {
// Critical section
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
// Thread was interrupted while waiting for the lock
Thread.currentThread().interrupt();
}
This is useful for implementing cancellable operations. With synchronized, a thread waiting for a monitor lock cannot be interrupted.
Monitoring and Diagnostics
ReentrantLock exposes state information useful for building watchdog services:
ReentrantLock lock = new ReentrantLock();
lock.getHoldCount(); // How many times current thread holds this lock
lock.isHeldByCurrentThread(); // Does the current thread own this lock?
lock.isLocked(); // Is any thread holding this lock?
lock.hasQueuedThreads(); // Are threads waiting for this lock?
lock.getQueueLength(); // How many threads are waiting?
ReentrantReadWriteLock
Separates read and write access. Multiple readers can hold the read lock simultaneously, but only one writer can hold the write lock (and no readers during a write).
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Cache<K, V> {
private final Map<K, V> map = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
public V get(K key) {
readLock.lock();
try {
return map.get(key);
} finally {
readLock.unlock();
}
}
public void put(K key, V value) {
writeLock.lock();
try {
map.put(key, value);
} finally {
writeLock.unlock();
}
}
public int size() {
readLock.lock();
try {
return map.size();
} finally {
readLock.unlock();
}
}
}
Lock Compatibility Matrix
| Read Lock Held | Write Lock Held | |
|---|---|---|
| Read Lock Request | ✓ Allowed | ✗ Blocked |
| Write Lock Request | ✗ Blocked | ✗ Blocked |
Performance Characteristics
- Read-heavy workloads (90%+ reads): ReadWriteLock provides up to 3x performance gain over exclusive locking.
- Write-heavy workloads: No benefit — use a regular ReentrantLock instead.
- Lock downgrade: A thread holding the write lock can acquire the read lock, then release the write lock (keeping only the read lock). The reverse (upgrading read to write) is not supported and will deadlock.
// Lock downgrade pattern
writeLock.lock();
try {
// Modify shared state
map.put(key, computeValue());
// Downgrade: acquire read lock before releasing write lock
readLock.lock();
} finally {
writeLock.unlock(); // Release write, keep read
}
try {
// Continue with read-only access
return map.get(key);
} finally {
readLock.unlock();
}
StampedLock (Java 8+)
An advanced lock that adds optimistic reading — readers don’t actually acquire a lock, they just check if a write occurred:
import java.util.concurrent.locks.StampedLock;
public class Point {
private double x, y;
private final StampedLock lock = new StampedLock();
public void move(double deltaX, double deltaY) {
long stamp = lock.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
lock.unlockWrite(stamp);
}
}
public double distanceFromOrigin() {
// Optimistic read — no lock acquired
long stamp = lock.tryOptimisticRead();
double currentX = x;
double currentY = y;
// Check if a write happened during our read
if (!lock.validate(stamp)) {
// Write occurred — fall back to regular read lock
stamp = lock.readLock();
try {
currentX = x;
currentY = y;
} finally {
lock.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
StampedLock Trade-offs
| Pros | Cons |
|---|---|
| Optimistic reads avoid lock contention entirely | Not reentrant — cannot re-enter from same thread |
| Higher throughput than ReadWriteLock for short reads | No Condition support |
| Supports lock conversion (read → write) | More complex API — easy to misuse |
| Not interruptible in default mode |
Conditions
Conditions are the explicit-lock equivalent of wait()/notify(). They allow threads to wait for specific events within a lock.
public class BoundedQueue<T> {
private final Queue<T> queue = new LinkedList<>();
private final int capacity;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public BoundedQueue(int capacity) {
this.capacity = capacity;
}
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // Wait until space is available
}
queue.add(item);
notEmpty.signal(); // Signal ONE waiting consumer
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // Wait until item is available
}
T item = queue.poll();
notFull.signal(); // Signal ONE waiting producer
return item;
} finally {
lock.unlock();
}
}
}
Conditions vs wait/notify
| Feature | wait()/notify() | Condition |
|---|---|---|
| Multiple wait sets per lock | ✗ (one per object) | ✓ (multiple Conditions per Lock) |
| Timed wait | wait(ms) | await(time, unit), awaitNanos(), awaitUntil(Date) |
| Interruptible | ✓ | ✓ + awaitUninterruptibly() |
| Signal specificity | notify() picks random | signal() picks random from THIS Condition |
| Spurious wakeups | Possible | Possible — always use while loop |
Key Condition Methods
| Method | Description |
|---|---|
await() | Release lock, sleep until signal or interrupt |
awaitUninterruptibly() | Like await but ignores interrupts |
awaitNanos(long nanos) | Timed await, returns remaining nanos |
await(long time, TimeUnit unit) | Timed await |
awaitUntil(Date deadline) | Wait until absolute time |
signal() | Wake up one waiting thread |
signalAll() | Wake up all waiting threads |
Critical Rule
signal() will only reach the awaiting thread after the signalling thread releases the lock:
lock.lock();
try {
// Modify shared state
dataReady = true;
condition.signal(); // Queues the wakeup — doesn't deliver yet
} finally {
lock.unlock(); // NOW the awaiting thread can actually wake up
}
Watchdog Pattern
For production systems, implement a lock monitoring service that detects hung threads:
public class LockWatchdog {
private final ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor();
private final Map<ReentrantLock, Long> lockAcquisitionTimes =
new ConcurrentHashMap<>();
private final long maxHoldTimeMs;
public LockWatchdog(long maxHoldTimeMs) {
this.maxHoldTimeMs = maxHoldTimeMs;
scheduler.scheduleAtFixedRate(this::checkLocks, 1, 1, TimeUnit.SECONDS);
}
public void registerLock(ReentrantLock lock) {
lockAcquisitionTimes.put(lock, System.currentTimeMillis());
}
public void unregisterLock(ReentrantLock lock) {
lockAcquisitionTimes.remove(lock);
}
private void checkLocks() {
long now = System.currentTimeMillis();
lockAcquisitionTimes.forEach((lock, acquiredAt) -> {
if (now - acquiredAt > maxHoldTimeMs && lock.isLocked()) {
System.err.println("WARNING: Lock held for " +
(now - acquiredAt) + "ms — possible deadlock");
// Log thread info, send alert, etc.
}
});
}
}
Choosing the Right Lock

Summary
- ReentrantLock: The go-to explicit lock. Use when you need tryLock, timeout, fairness, or conditions.
- ReentrantReadWriteLock: Use when reads vastly outnumber writes (3x speedup possible).
- StampedLock: Use for ultra-high read throughput with optimistic reads. Not reentrant.
- Conditions: The explicit-lock version of wait/notify, with support for multiple wait sets per lock.
- Always unlock in a
finallyblock. Always. - Fair locks prevent starvation but cost throughput.