☕ Java

ReentrantLock

ReentrantLock is the primary Lock implementation in java.util.concurrent.locks and the direct replacement for synchronized when more control over locking is needed. Like Java's intrinsic locks, it is reentrant — the same thread can acquire it multiple times without deadlocking, and must release it the same number of times to fully unlock. Unlike intrinsic locks, ReentrantLock exposes its reentrancy count, supports timed and interruptible lock acquisition, allows inspection of lock state and waiting threads, and can be constructed in fair mode (FIFO ordering) or the default unfair mode (higher throughput). ReentrantLock is backed by AbstractQueuedSynchronizer (AQS), which maintains a CLH queue of waiting threads and uses LockSupport.park/unpark for blocking and waking. This entry covers construction and fair vs unfair mode in depth, all acquisition methods with timing and interruption semantics, reentrancy mechanics and hold count, lock inspection methods, performance characteristics, and the canonical patterns for safe ReentrantLock usage in real concurrent systems.

Construction, Fairness, and Acquisition Methods

ReentrantLock is constructed with new ReentrantLock() (unfair mode, default) or new ReentrantLock(true) (fair mode). In unfair mode, a thread that just released the lock may immediately re-acquire it before threads that have been waiting in the queue — a barging thread gets priority over queued threads. This maximizes throughput because it avoids context-switching to wake a queued thread when a running thread can take the lock directly. In fair mode, the lock is always granted to the longest-waiting thread in the CLH queue, providing strict FIFO ordering and preventing starvation at the cost of 5-10x lower throughput under high contention. lock() acquires the lock unconditionally, blocking the calling thread until the lock is available. If the calling thread already holds the lock, lock() succeeds immediately and increments the hold count. lock() does not throw InterruptedException — it ignores thread interruption while waiting. This makes it equivalent to entering a synchronized block in terms of interruption behavior. lockInterruptibly() acquires the lock but responds to interruption: if the thread is interrupted while waiting (before it acquires the lock), it throws InterruptedException and does not acquire the lock. If the thread is interrupted after it has acquired the lock, the interrupt status is set but no exception is thrown — the thread continues holding the lock normally. lockInterruptibly() enables task cancellation in lock-waiting scenarios: a thread blocked waiting for a lock can be unblocked by calling interrupt() on it, allowing cancellation of work that is deadlocked or taking too long. tryLock() immediately attempts the lock without waiting. It returns true if the lock was acquired and false if it was held by another thread. tryLock() in unfair mode will barge the queue even in a fair-mode lock — it ignores the fairness setting, which is usually the desired behavior when you want a non-blocking check. tryLock(long timeout, TimeUnit unit) waits up to the specified duration, responding to interruption during the wait.
Java
// ── Construction: fair vs unfair ─────────────────────────────────────
ReentrantLock unfairLock = new ReentrantLock();        // default: unfair (higher throughput)
ReentrantLock fairLock   = new ReentrantLock(true);    // fair: FIFO, prevents starvation

// ── lock() — blocks until acquired, ignores interruption ──────────────
ReentrantLock lock = new ReentrantLock();

lock.lock();
try {
    performCriticalWork();
} finally {
    lock.unlock();   // ALWAYS in finally
}

// ── lockInterruptibly() — cancellable lock wait ────────────────────────
public void cancellableOperation(ReentrantLock lock) throws InterruptedException {
    lock.lockInterruptibly();   // throws InterruptedException if thread is interrupted while waiting
    try {
        performCriticalWork();
    } finally {
        lock.unlock();
    }
}

Thread worker = new Thread(() -> {
    try {
        cancellableOperation(lock);
        System.out.println("Work completed");
    } catch (InterruptedException e) {
        System.out.println("Operation cancelled — was waiting for lock");
        Thread.currentThread().interrupt();
    }
});
lock.lock();          // hold the lock so worker must wait
worker.start();
Thread.sleep(200);
worker.interrupt();   // cancels worker's wait on lockInterruptibly()
Thread.sleep(100);
lock.unlock();

// ── tryLock() — non-blocking ──────────────────────────────────────────
if (lock.tryLock()) {
    try {
        System.out.println("Got lock immediately");
    } finally {
        lock.unlock();
    }
} else {
    System.out.println("Lock busy — skip or retry");
}

// ── tryLock(timeout) — give up after waiting ──────────────────────────
try {
    if (lock.tryLock(1, TimeUnit.SECONDS)) {
        try {
            System.out.println("Got lock within 1 second");
        } finally {
            lock.unlock();
        }
    } else {
        System.out.println("Could not acquire lock in 1 second — aborting");
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

// ── Deadlock breaking with tryLock ────────────────────────────────────
public static boolean transferSafe(ReentrantLock lockA, ReentrantLock lockB, Runnable work)
        throws InterruptedException {
    while (true) {
        if (lockA.tryLock(100, TimeUnit.MILLISECONDS)) {
            try {
                if (lockB.tryLock(100, TimeUnit.MILLISECONDS)) {
                    try {
                        work.run();
                        return true;
                    } finally { lockB.unlock(); }
                }
                // lockB not acquired — release lockA and retry after backoff
            } finally { lockA.unlock(); }
        }
        Thread.sleep(1 + ThreadLocalRandom.current().nextLong(10));  // random backoff
    }
}

Reentrancy, Hold Count, and Lock Inspection

Reentrancy means a thread that already holds a ReentrantLock can call lock() again without blocking. The lock tracks a hold count: each successful lock() or tryLock() increments the count; each unlock() decrements it. The lock is fully released only when the hold count returns to zero. This mirrors Java's intrinsic lock reentrancy, which is necessary for synchronized methods that call other synchronized methods on the same object. ReentrantLock exposes introspection methods not available on intrinsic locks. isHeldByCurrentThread() returns true if the current thread holds the lock — useful for assertions and debugging. getHoldCount() returns the number of times the current thread holds the lock (the reentrancy depth). isLocked() returns true if any thread holds the lock. getQueueLength() returns an estimate of the number of threads waiting to acquire the lock. hasQueuedThread(Thread t) tests whether a specific thread is waiting. hasWaiters(Condition c) and getWaitQueueLength(Condition c) provide information about threads waiting on a specific Condition. These inspection methods are designed for monitoring and debugging, not for making synchronization decisions. iif (lock.isLocked()) { ... } is not a safe substitute for lock.tryLock() — the lock state can change between the isLocked() check and any action taken on its result. The inspection methods return best-effort estimates under concurrent conditions. The getHoldCount() and isHeldByCurrentThread() methods are directly useful in reentrant scenarios where a method needs to conditionally acquire a lock it might already hold. They enable patterns like "acquire the lock only if I don't already hold it" — useful when a public method acquires a lock and then calls a private method that can be called either from the synchronized public method or from external code directly.
Java
// ── Reentrancy: same thread can lock multiple times ──────────────────
ReentrantLock lock = new ReentrantLock();

lock.lock();                              // hold count: 1
try {
    System.out.println("Outer: " + lock.getHoldCount());   // 1
    lock.lock();                          // hold count: 2 — does NOT block
    try {
        System.out.println("Inner: " + lock.getHoldCount()); // 2
        lock.lock();                      // hold count: 3
        try {
            System.out.println("Deepest: " + lock.getHoldCount()); // 3
        } finally {
            lock.unlock();                // hold count: 2
        }
    } finally {
        lock.unlock();                    // hold count: 1
    }
    System.out.println("Back to outer: " + lock.getHoldCount()); // 1
} finally {
    lock.unlock();                        // hold count: 0 — lock fully released
}

// ── isHeldByCurrentThread: conditional acquisition ────────────────────
public class ResourceManager {
    private final ReentrantLock lock = new ReentrantLock();

    // Public API: always acquires the lock
    public void processResource() {
        lock.lock();
        try {
            doInternalWork();   // may call processResource() recursively — safe due to reentrancy
        } finally {
            lock.unlock();
        }
    }

    // Can be called from processResource() (already locked) or standalone:
    private void doInternalWork() {
        // If called from processResource: isHeldByCurrentThread = true, getHoldCount >= 1
        // If called standalone: isHeldByCurrentThread = false
        System.out.println("Hold count: " + lock.getHoldCount());

        // Assertion-style check — verify lock is held before internal work:
        assert lock.isHeldByCurrentThread() : "doInternalWork must be called while holding lock";

        // Work that requires the lock...
    }
}

// ── Lock inspection methods ───────────────────────────────────────────
ReentrantLock inspectedLock = new ReentrantLock();

System.out.println("isLocked: "             + inspectedLock.isLocked());           // false
System.out.println("isHeldByCurrentThread: "+ inspectedLock.isHeldByCurrentThread()); // false
System.out.println("getHoldCount: "         + inspectedLock.getHoldCount());       // 0
System.out.println("getQueueLength: "       + inspectedLock.getQueueLength());     // 0
System.out.println("isFair: "              + inspectedLock.isFair());             // depends on constructor

inspectedLock.lock();
System.out.println("After lock():");
System.out.println("  isLocked: "           + inspectedLock.isLocked());           // true
System.out.println("  isHeldByCurrentThread:"+ inspectedLock.isHeldByCurrentThread()); // true
System.out.println("  getHoldCount: "       + inspectedLock.getHoldCount());       // 1
inspectedLock.unlock();

// hasQueuedThread and getQueueLength:
Thread waiter = new Thread(() -> {
    inspectedLock.lock();
    try { Thread.sleep(100); } catch (InterruptedException e) {}
    finally { inspectedLock.unlock(); }
});
inspectedLock.lock();   // main thread holds lock
waiter.start();
Thread.sleep(50);       // give waiter time to start waiting
System.out.println("Queue length: " + inspectedLock.getQueueLength());   // 1
System.out.println("Waiter queued: " + inspectedLock.hasQueuedThread(waiter)); // true
inspectedLock.unlock();
waiter.join();

ReentrantLock vs synchronized — When to Use Each

The choice between ReentrantLock and synchronized should be based on specific requirements, not habit. synchronized is the preferred choice for the majority of cases: it is syntactically cleaner, automatically releases on exception without requiring a finally block, cannot be misused by forgetting unlock(), integrates with the JVM's bias locking and adaptive spinning optimizations, and is universally understood. The performance difference between synchronized and ReentrantLock under low contention is negligible on modern JVMs — both benefit from lock elision and biased locking. ReentrantLock should be chosen when specific features are needed that synchronized cannot provide. Timed lock acquisition (tryLock with timeout) is necessary for deadlock avoidance by lock ordering with backoff — if acquiring a second lock times out, all locks can be released and retried. Interruptible lock acquisition (lockInterruptibly) is necessary when threads must be cancellable while waiting for a lock. Multiple Conditions per lock are necessary for producer-consumer implementations with separate conditions, where Object.notifyAll() would cause a thundering herd. Fair scheduling (new ReentrantLock(true)) is necessary when no thread must be starved, at the cost of throughput. Lock state inspection (isLocked, getQueueLength) is useful for monitoring and adaptive behavior. Performance under contention: ReentrantLock with unfair mode often outperforms synchronized under high contention because its CLH queue has better cache behavior and its park/unpark mechanism has lower overhead than the OS-level parking used by synchronized's heavyweight monitor. This advantage is measurable only under sustained high contention — for most application code, the difference is in the noise. A practical rule: start with synchronized. Switch to ReentrantLock when you hit a specific requirement from the list above and have confirmed that synchronized cannot meet it. Never use ReentrantLock "just in case" or because it seems more professional — the complexity cost (required finally blocks, risk of lock leaks) is real.
Java
// ── Decision: when synchronized is sufficient ────────────────────────
public class SimpleSafeCounter {
    private int count = 0;

    // synchronized is perfectly correct and simpler here:
    public synchronized void increment() { count++; }
    public synchronized int get()        { return count; }
}

// ── Decision: ReentrantLock needed for timed acquisition ──────────────
public class ResourcePool {
    private final Queue<Resource> available = new ArrayDeque<>();
    private final ReentrantLock  lock       = new ReentrantLock();

    public Optional<Resource> acquire(long timeoutMs) throws InterruptedException {
        if (!lock.tryLock(timeoutMs, TimeUnit.MILLISECONDS)) {
            return Optional.empty();   // could not get pool lock — caller can try again
        }
        try {
            return available.isEmpty()
                ? Optional.empty()
                : Optional.of(available.poll());
        } finally {
            lock.unlock();
        }
    }

    public void release(Resource r) {
        lock.lock();
        try { available.offer(r); }
        finally { lock.unlock(); }
    }
}

// ── Decision: ReentrantLock needed for interruptible wait ─────────────
public class CancellableProcessor {
    private final ReentrantLock processingLock = new ReentrantLock();

    public void process(Task task) throws InterruptedException {
        processingLock.lockInterruptibly();   // can be cancelled while waiting
        try {
            task.execute();
        } finally {
            processingLock.unlock();
        }
    }
}

// ── Decision: fair lock for priority-sensitive work ───────────────────
public class FairWorkQueue {
    private final Queue<Runnable>  tasks    = new ArrayDeque<>();
    private final ReentrantLock    lock     = new ReentrantLock(true);  // fair
    private final Condition        notEmpty = lock.newCondition();

    public void submit(Runnable task) {
        lock.lock();
        try {
            tasks.add(task);
            notEmpty.signal();
        } finally { lock.unlock(); }
    }

    // Fair lock ensures worker threads get work in submission order:
    public Runnable take() throws InterruptedException {
        lock.lock();
        try {
            while (tasks.isEmpty()) notEmpty.await();
            return tasks.poll();
        } finally { lock.unlock(); }
    }
}

// ── Monitoring ReentrantLock for operational insight ──────────────────
public class MonitoredService {
    private final ReentrantLock lock    = new ReentrantLock();
    private final AtomicLong    blocked = new AtomicLong(0);
    private final AtomicLong    total   = new AtomicLong(0);

    public void doWork() {
        total.incrementAndGet();
        int queueDepthAtEntry = lock.getQueueLength();
        if (queueDepthAtEntry > 0) blocked.incrementAndGet();

        lock.lock();
        try {
            performWork();
        } finally {
            lock.unlock();
        }
    }

    public double contentionRate() {
        long t = total.get();
        return t == 0 ? 0 : (double) blocked.get() / t;
    }
}

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.