Locks API
The java.util.concurrent.locks package, introduced in Java 5, provides a richer and more flexible locking framework than Java's built-in synchronized keyword. While synchronized is sufficient for many concurrency needs, it has fundamental limitations: it cannot attempt a lock without blocking forever, cannot be interrupted while waiting for a lock, cannot have multiple wait conditions per lock, and always uses unfair scheduling. The Locks API addresses all of these with explicit Lock objects that support timed tryLock(), interruptible lock acquisition, multiple Condition objects per lock, and configurable fairness. The package defines the Lock interface (implemented by ReentrantLock), the ReadWriteLock interface (implemented by ReentrantReadWriteLock), the Condition interface (replacing Object.wait/notify), LockSupport for building custom synchronizers, and AbstractQueuedSynchronizer (AQS) — the internal engine behind nearly every concurrent utility in java.util.concurrent. This entry covers the Lock interface contract in full, how the Locks API compares to synchronized, the pattern for correct lock usage with finally-unlock, timed and interruptible acquisition, LockSupport primitives, and a map of which implementation to reach for in which situation.
The Lock Interface — Contract and Comparison to synchronized
// ── Lock interface — the six methods ─────────────────────────────────
import java.util.concurrent.locks.*;
import java.util.concurrent.TimeUnit;
Lock lock = new ReentrantLock();
// ── lock() — basic acquisition, blocks until available ────────────────
lock.lock();
try {
// critical section — only one thread here at a time
} finally {
lock.unlock(); // ALWAYS in finally — guaranteed release even on exception
}
// ── tryLock() — non-blocking attempt ──────────────────────────────────
if (lock.tryLock()) {
try {
System.out.println("Lock acquired — doing work");
} finally {
lock.unlock();
}
} else {
System.out.println("Lock not available — doing something else");
}
// ── tryLock(time, unit) — timed attempt ───────────────────────────────
try {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
System.out.println("Acquired within 500ms");
} finally {
lock.unlock();
}
} else {
System.out.println("Timed out — could not acquire in 500ms");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Interrupted while waiting");
}
// ── lockInterruptibly() — cancellable acquisition ─────────────────────
Thread worker = new Thread(() -> {
try {
lock.lockInterruptibly(); // throws InterruptedException if interrupted while waiting
try {
System.out.println("Lock acquired");
Thread.sleep(2000);
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("Interrupted while waiting for lock — task cancelled");
Thread.currentThread().interrupt();
}
});
worker.start();
Thread.sleep(100);
worker.interrupt(); // worker wakes up from lockInterruptibly() with InterruptedException
// ── WRONG: unlock() inside try block — not guaranteed to run ──────────
lock.lock();
try {
doWork();
lock.unlock(); // ✗ — if doWork() throws, unlock is never called — lock held forever
} catch (Exception e) { }
// ── CORRECT: unlock() in finally ──────────────────────────────────────
lock.lock();
try {
doWork(); // exception here is caught by finally
} finally {
lock.unlock(); // ✓ — always runs
}
// ── WRONG: lock() inside try block — if lock() throws, unlock on unheld lock ──
try {
lock.lock(); // ✗ — if lock() itself throws (e.g. Error), finally runs unlock on
doWork(); // a lock we don't hold — IllegalMonitorStateException
} finally {
lock.unlock(); // might not hold the lock!
}
// ── CORRECT: lock() before try ────────────────────────────────────────
lock.lock(); // ✓ — outside try; if this throws, we don't hold the lock
try {
doWork();
} finally {
lock.unlock(); // only reached if lock() succeeded
}Condition — Multiple Wait Sets Per Lock
// ── Bounded buffer using two Conditions ─────────────────────────────
public class BoundedBuffer<T> {
private final Queue<T> queue = new ArrayDeque<>();
private final int capacity;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // producers wait here
private final Condition notEmpty = lock.newCondition(); // consumers wait here
public BoundedBuffer(int capacity) { this.capacity = capacity; }
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // releases lock, waits in notFull's wait set
}
queue.add(item);
notEmpty.signal(); // wakes exactly ONE consumer — no thundering herd
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // releases lock, waits in notEmpty's wait set
}
T item = queue.poll();
notFull.signal(); // wakes exactly ONE producer
return item;
} finally {
lock.unlock();
}
}
}
// ── Condition.awaitNanos: timed wait with remaining time ──────────────
public T pollWithTimeout(long timeoutNanos) throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
if (timeoutNanos <= 0) return null; // timed out
timeoutNanos = notEmpty.awaitNanos(timeoutNanos); // returns remaining time
}
T item = queue.poll();
notFull.signal();
return item;
} finally {
lock.unlock();
}
}
// ── Condition.awaitUninterruptibly: wait without InterruptedException ──
public T takeUninterruptible() {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.awaitUninterruptibly(); // ignores interruption, does not throw
}
T item = queue.poll();
notFull.signal();
return item;
} finally {
lock.unlock();
}
}
// ── Condition vs Object.wait/notify comparison ────────────────────────
// Object monitor (one wait set for all conditions):
synchronized (lock) {
while (!canProduce) lock.wait(); // producer and consumer share the same wait set
produce();
lock.notifyAll(); // wakes EVERYONE — consumers AND producers — thundering herd
}
// Condition (separate wait sets per condition):
lock.lock();
try {
while (!canProduce) notFull.await(); // producers only in notFull wait set
produce();
notEmpty.signal(); // wakes ONE consumer — precise, efficient
} finally { lock.unlock(); }
// ── Multiple Conditions for a state machine ───────────────────────────
public class TrafficLight {
private enum Color { RED, GREEN, YELLOW }
private Color current = Color.RED;
private final ReentrantLock lock = new ReentrantLock();
private final Condition isGreen = lock.newCondition();
private final Condition isRed = lock.newCondition();
private final Condition isYellow = lock.newCondition();
public void waitForGreen() throws InterruptedException {
lock.lock();
try {
while (current != Color.GREEN) isGreen.await();
} finally { lock.unlock(); }
}
public void changeToGreen() {
lock.lock();
try {
current = Color.GREEN;
isGreen.signalAll(); // wake all threads waiting for green
} finally { lock.unlock(); }
}
public void changeToRed() {
lock.lock();
try {
current = Color.RED;
isRed.signalAll();
} finally { lock.unlock(); }
}
}LockSupport and AbstractQueuedSynchronizer
// ── LockSupport.park and unpark ───────────────────────────────────────
import java.util.concurrent.locks.LockSupport;
Thread parker = new Thread(() -> {
System.out.println("Thread: about to park");
LockSupport.park(); // blocks until unpark() or interrupt
System.out.println("Thread: unparked");
}, "ParkThread");
parker.start();
Thread.sleep(500);
System.out.println("Main: calling unpark");
LockSupport.unpark(parker); // releases parker — sets its permit
// ── Permit accumulation: unpark before park is safe ───────────────────
Thread future = new Thread(() -> {
try { Thread.sleep(200); } catch (InterruptedException e) {}
System.out.println("Thread: parking — should return immediately");
LockSupport.park(); // returns immediately because unpark was called first
System.out.println("Thread: done");
});
future.start();
LockSupport.unpark(future); // called BEFORE future parks — permit stored
// Future's park() returns immediately because permit was pre-stored
// ── park with blocker object — visible in thread dumps ────────────────
Object blocker = new Object();
Thread tracked = new Thread(() -> {
LockSupport.park(blocker); // blocker appears in thread dump:
// "parking to wait for <0x...> (a java.lang.Object)"
});
tracked.start();
Thread.sleep(100);
// jstack output for tracked thread:
// at sun.misc.Unsafe.park(Native Method)
// - parking to wait for <0x...> (a java.lang.Object) ← blocker visible
LockSupport.unpark(tracked);
// ── Custom synchronizer using AQS ─────────────────────────────────────
// A simple binary mutex (equivalent to a 1-permit Semaphore) built on AQS:
public class BinaryMutex {
private final Sync sync = new Sync();
private static class Sync extends AbstractQueuedSynchronizer {
// State: 0 = unlocked, 1 = locked
@Override
protected boolean tryAcquire(int arg) {
// CAS state from 0 to 1: succeeds only if currently unlocked
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0); // release: set state back to 0
return true;
}
@Override
protected boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
Condition newCondition() { return new ConditionObject(); }
}
public void lock() { sync.acquire(1); }
public void unlock() { sync.release(1); }
public boolean tryLock() { return sync.tryAcquire(1); }
public Condition newCondition() { return sync.newCondition(); }
}
// ── AQS shared mode: a counting semaphore ────────────────────────────
// (simplified — java.util.concurrent.Semaphore uses AQS internally)
public class SimpleSemaphore {
private final Sync sync;
private static class Sync extends AbstractQueuedSynchronizer {
Sync(int permits) { setState(permits); }
@Override
protected int tryAcquireShared(int arg) {
for (;;) {
int current = getState();
int next = current - arg;
if (next < 0) return next; // negative = fail (no permits)
if (compareAndSetState(current, next)) return next; // success
}
}
@Override
protected boolean tryReleaseShared(int arg) {
for (;;) {
int current = getState();
int next = current + arg;
if (compareAndSetState(current, next)) return true;
}
}
}
public SimpleSemaphore(int permits) { sync = new Sync(permits); }
public void acquire() throws InterruptedException { sync.acquireSharedInterruptibly(1); }
public void release() { sync.releaseShared(1); }
}