☕ Java

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

The Lock interface defines six methods. lock() acquires the lock, blocking until it is available — equivalent to entering a synchronized block. unlock() releases the lock — equivalent to exiting a synchronized block. tryLock() attempts to acquire the lock immediately, returning true if successful and false if the lock is currently held by another thread. tryLock(long time, TimeUnit unit) attempts acquisition for up to the specified duration, returning true if acquired within the time limit and false otherwise. lockInterruptibly() acquires the lock, blocking until available, but throws InterruptedException if the waiting thread is interrupted — unlike lock() and synchronized, which ignore interruption while waiting. newCondition() returns a Condition object bound to this lock, providing the await/signal/signalAll equivalents of Object.wait/notify/notifyAll. The critical difference from synchronized: the Lock API requires explicit unlocking. A synchronized block automatically releases the lock when control leaves the block — by normal completion, return, or exception. An explicit Lock does not. If a thread acquires a lock and then throws an exception without unlocking, the lock is held forever — deadlocking every other thread that needs it. The mandatory pattern is: lock(), try { ... critical section ... } finally { unlock(); }. The finally block guarantees that unlock() is called regardless of how the try block exits. Forgetting the finally block or calling unlock() inside the try block (where it might not execute if an exception is thrown before it) is a common and serious error. The advantages of explicit Lock over synchronized are specific and measurable. Timed lock acquisition (tryLock with timeout) breaks the deadlock-prevention deadlock: a thread can give up trying to acquire a set of locks and retry, preventing circular waits. Interruptible lock acquisition (lockInterruptibly) allows a blocked thread to be cancelled, which synchronized cannot provide. Multiple Condition objects per lock allow precise signaling in producer-consumer patterns without the thundering herd problem. Fairness control via the constructor parameter guarantees FIFO scheduling when needed. Performance advantages exist under high contention due to the CLH queue used internally. When synchronized is sufficient — which is the majority of cases — it should be preferred: it is syntactically simpler, automatically releases on exception, cannot be misused by forgetting unlock(), and the JVM can apply biased locking and adaptive spinning optimizations that often make it faster than explicit locks in low-contention scenarios.
Java
// ── 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

A Condition object is obtained from a Lock via lock.newCondition() and represents a single condition variable associated with that lock. A lock can have any number of Conditions, each maintaining its own independent wait set — the set of threads suspended on that condition. This is the key advantage over Object.wait/notify, which provides only one wait set per monitor. Separate Conditions for "not full" and "not empty" allow a producer to wake precisely one consumer (notEmpty.signal()) without disturbing waiting producers, eliminating the thundering herd caused by Object.notifyAll(). The Condition methods mirror Object's wait/notify: await() is equivalent to Object.wait() — releases the lock, suspends the calling thread, and adds it to the Condition's wait set. signal() is equivalent to Object.notify() — wakes one thread from this Condition's wait set. signalAll() is equivalent to Object.notifyAll() — wakes all threads in this Condition's wait set. The same spurious wakeup contract applies: await() may return even without a signal, so await() must always be called in a while loop that re-checks the condition. Additional Condition methods not available in Object: awaitUninterruptibly() waits without throwing InterruptedException — useful when interruption should not cancel the wait; awaitUntil(Date deadline) waits until an absolute deadline; awaitNanos(long nanosTimeout) waits for a precise duration and returns the remaining wait time. These richer forms allow more sophisticated timeout management than Object.wait(timeout). The Condition is always used in conjunction with its lock: await() must be called while holding the lock (otherwise IllegalMonitorStateException), and it atomically releases the lock and enters the wait set, just as Object.wait() does. signal() and signalAll() must also be called while holding the lock. The ownership requirement and atomicity guarantees are identical to synchronized — the difference is the richness of the API and the ability to have multiple independent Conditions per lock.
Java
// ── 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 is the primitive building block for the entire java.util.concurrent synchronizer infrastructure. It provides two fundamental operations: LockSupport.park() causes the current thread to block until a "permit" is available or the thread is interrupted. LockSupport.unpark(Thread t) grants a permit to thread t, unblocking it if it is parked or causing its next park() to return immediately. Permits are binary — multiple unpark() calls before a park() result in exactly one immediate return from park(). Unlike Object.wait(), park() does not require holding any lock. It can be called at any time without a synchronized context, making it suitable for building lock-free and non-blocking synchronizers. park() also does not guarantee that it returns only due to unpark() — it may return spuriously, for the same reasons as Object.wait(), so callers must always check the condition after park() returns. LockSupport.parkNanos(long nanos) and LockSupport.parkUntil(long deadline) provide timed blocking. LockSupport.park(Object blocker) accepts a blocker object that becomes visible in thread dumps, making it possible to identify why a thread is parked — thread dumps show the blocker object, which is why blocked threads in modern Java often show a recognizable ReentrantLock or Condition object rather than an anonymous park(). AbstractQueuedSynchronizer (AQS) is the framework behind ReentrantLock, ReentrantReadWriteLock, Semaphore, CountDownLatch, CyclicBarrier, and others. AQS maintains an integer state (acquired via getState/setState/compareAndSetState) and a CLH wait queue of threads. Subclasses implement tryAcquire() and tryRelease() (for exclusive locks) or tryAcquireShared() and tryReleaseShared() (for shared locks like read locks and semaphores) to define their specific synchronization semantics. AQS handles all queueing, blocking via LockSupport.park(), waking via LockSupport.unpark(), cancellation, and timeout — the subclass only defines when acquire succeeds or fails.
Java
// ── 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); }
}

Related Topics in Multithreading

Process vs Thread
A process is an independent program in execution with its own isolated memory space, file handles, and system resources, managed by the operating system and separated from all other processes by strict boundaries. A thread is a unit of execution that lives inside a process, sharing that process's memory, heap, and resources with every other thread in the same process. Java programs run inside a JVM process; the JVM itself creates and manages threads, and every Java application starts with at least one thread — the main thread — with additional threads created by the JVM for garbage collection, JIT compilation, signal handling, and other runtime tasks. Understanding the distinction between processes and threads is the foundation for all concurrent programming in Java: it determines what is shared and what is isolated, what is fast and what is expensive, what fails independently and what fails together. This entry covers the OS-level and JVM-level model of processes and threads, the memory model that follows from the shared-versus-isolated distinction, the cost model for creation and context switching, failure isolation and its consequences, inter-process and inter-thread communication mechanisms, and the practical decision of when to use multiple processes versus multiple threads.
Thread Basics
A Java thread is an instance of java.lang.Thread that represents an independent path of execution within the JVM process. Every thread has a lifecycle — from creation through runnable, running, blocked, waiting, timed-waiting, and terminated states — and a set of properties including its name, priority, daemon status, thread group, and uncaught exception handler. The Java memory model specifies what visibility guarantees exist between threads and when writes by one thread are guaranteed to be visible to another. Thread scheduling is controlled by the OS scheduler subject to hints from the JVM via thread priority; the JVM does not provide real-time scheduling guarantees. This entry covers the complete thread lifecycle and its state machine, thread properties and how they affect scheduling and JVM shutdown, the happens-before relationship and why it matters for visibility, daemon threads and their relationship to JVM shutdown, thread interruption as a cooperative cancellation mechanism, and the methods on Thread that every Java developer must understand.
Creating Threads
Java provides three primary abstractions for defining the work a thread will execute: the Thread class itself (subclassed to override run()), the Runnable interface (a task with no return value and no checked exception), and the Callable interface (a task with a return value and a declared checked exception). Each represents a different contract between the task and the infrastructure that runs it. Thread subclassing couples the task definition to the execution mechanism and is the oldest and least flexible approach. Runnable decouples the task from the thread, allowing the same Runnable to be submitted to thread pools, scheduled executors, or wrapped in Thread objects. Callable extends that decoupling to include a return value and exception propagation, returning a Future that allows the caller to retrieve the result or handle exceptions asynchronously. Understanding all three — their contracts, their limitations, and when to use each — is the foundation of concurrent programming in Java before reaching for higher-level constructs.
Thread Lifecycle
The Java thread lifecycle is the complete sequence of states a thread passes through from the moment a Thread object is constructed to the moment its execution ends. Java defines six states in the Thread.State enum — NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED — and the JVM transitions threads between these states in response to specific method calls, lock acquisitions, monitor notifications, timeouts, and exceptions. Each state has a precise meaning, a defined set of entry conditions, and a defined set of exit conditions. Understanding the lifecycle in full is prerequisite knowledge for diagnosing deadlocks, thread leaks, performance bottlenecks in thread dumps, and incorrect synchronization — all of which manifest as threads stuck in specific states. This entry covers every state in the lifecycle with its entry and exit conditions, all legal and illegal state transitions, how thread dumps represent each state, the interaction between lifecycle states and interruption, the effect of uncaught exceptions on lifecycle, and how to observe lifecycle transitions programmatically.