skip to content

What is ReentrantLock, and how does it differ from the synchronized keyword?

level: middleimportance: must knowfreq 78%

answer

  1. Explicit lock() / unlock() vs automatic synchronized
  2. unlock() MUST be in finally; lock() outside the try
  3. Powers: tryLock, timeout, lockInterruptibly, fairness, multiple Conditions
  4. Both are reentrant (hold count)
  5. Built on AbstractQueuedSynchronizer (AQS)

basics

~20 s

ReentrantLock is a lock object you control with explicit lock() and unlock() calls. synchronized is a keyword that locks and unlocks automatically. ReentrantLock adds extra abilities like timed and interruptible locking, but you must remember to unlock it yourself in a finally block.

solid answer

~40 s

ReentrantLock is an explicit lock from java.util.concurrent.locks that replaces synchronized when you need more control. Both give mutual exclusion and are reentrant (the holding thread can re-acquire). The difference is flexibility: synchronized acquires and releases automatically at block boundaries and is simple, but it always blocks, can't be timed, and can't be interrupted while waiting. ReentrantLock adds tryLock() (non-blocking or with a timeout), lockInterruptibly() (cancelable waits), an optional fairness policy, and multiple Condition wait-sets per lock. The cost is discipline: you must call unlock() in a finally block, or a thrown exception leaves the lock held forever, deadlocking everyone. Use synchronized by default; reach for ReentrantLock only when you need timeouts, interruptibility, fairness, or several conditions.

code

java · 11 lines
java
private final Lock lock = new ReentrantLock();

void transfer(Account to, long amount) {
    lock.lock();              // acquire OUTSIDE the try
    try {
        balance -= amount;    // critical section
        to.deposit(amount);
    } finally {
        lock.unlock();        // release ALWAYS, even on exception
    }
}

go deeper

for a junior

Knows ReentrantLock needs explicit lock()/unlock() while synchronized is automatic, and that unlock() belongs in finally.

for a middle

Can list the extra capabilities (tryLock, timeout, lockInterruptibly, fairness, conditions), states both are reentrant, and applies the lock-in-finally idiom correctly.

for a senior

Articulates the 'prefer synchronized, use ReentrantLock for specific needs' guidance, explains why lock() goes outside the try, and notes the manual-release deadlock risk.

for a principal

Discusses the AQS foundation, uncontended vs contended fast/slow paths, fairness-vs-throughput trade-offs, and sets team conventions for when explicit locks are justified versus higher-level java.util.concurrent abstractions.

## The problem locks solve When multiple **threads** (independent paths of execution running concurrently) read and write the same data, their operations can interleave and corrupt that data — a **race condition**. A **lock** (also called a **mutex**, for mutual exclusion) fixes this: only one thread may hold the lock at a time, so the code it guards (the **critical section**) runs as if single-threaded. Java has had a built-in locking mechanism since version 1.0: the **`synchronized`** keyword. Every Java object carries a hidden lock called its **intrinsic lock** or **monitor**. Writing `synchronized(obj) { ... }` acquires `obj`'s monitor on entry and releases it on exit (including when an exception is thrown). It is simple and the release is automatic. ## What ReentrantLock is Java 5 added `java.util.concurrent.locks.ReentrantLock`, an **explicit lock**: a plain object implementing the `Lock` interface that you acquire and release with method calls instead of a language keyword. ```java Lock lock = new ReentrantLock(); lock.lock(); // acquire try { // critical section } finally { lock.unlock(); // release — MUST be in finally } ``` **"Reentrant"** means a thread that already holds the lock can acquire it again without deadlocking itself; the lock keeps a hold count and only truly releases when `unlock()` has been called as many times as `lock()`. This matters when one guarded method calls another guarded method on the same object. (Note: `synchronized` is *also* reentrant — the name just makes the property explicit.) ## Why ReentrantLock exists — the extra powers `synchronized` can only do one thing: block until it gets the lock, with no way to give up, time out, or be interrupted. `ReentrantLock` exposes a richer API: - **`tryLock()`** — attempt to acquire and return immediately with `true`/`false` instead of blocking. Lets you take an alternative action rather than wait. - **`tryLock(time, unit)`** — wait up to a bounded time, then give up. Prevents indefinite stalls. - **`lockInterruptibly()`** — block, but throw `InterruptedException` if the thread is interrupted while waiting, so a waiting thread can be cancelled. A thread blocked on `synchronized` cannot be interrupted out of the wait. - **Fairness** — `new ReentrantLock(true)` grants the lock in roughly first-come-first-served order; the default (`false`) is unfair and faster. - **Multiple `Condition`s** — `lock.newCondition()` creates a named wait-set; you can have several per lock (e.g. "not full" and "not empty" for a bounded buffer), whereas `synchronized` gives every object exactly one wait-set via `wait()`/`notify()`. - **Introspection** — `isLocked()`, `isHeldByCurrentThread()`, `getHoldCount()`, `getQueueLength()`. ## The trade-off The price for this power is **manual release**. With `synchronized`, the JVM releases the monitor automatically at the end of the block no matter how you leave it. With `ReentrantLock`, if you forget `unlock()`, or an exception skips it, the lock stays held forever and every other thread blocks permanently — a classic deadlock. Hence the iron rule: **always `unlock()` in a `finally`**, and call `lock()` *outside* the try (if `lock()` itself threw, you'd wrongly unlock a lock you never acquired). ## Which to choose Guidance from *Effective Java* and *Java Concurrency in Practice*: **prefer `synchronized`** for its simplicity and automatic release. Reach for `ReentrantLock` only when you genuinely need one of its extra features — a timeout, interruptible acquisition, fairness, non-blocking attempts, or multiple condition wait-sets. Performance is no longer a deciding factor: modern JVMs optimize uncontended `synchronized` heavily, and the two are comparable. ## Under the hood `ReentrantLock` is built on `AbstractQueuedSynchronizer` (AQS), a framework that maintains an atomic integer state (the hold count) plus a FIFO queue of waiting threads, using compare-and-swap for the fast uncontended path and `LockSupport.park` to suspend contended threads. You don't need this to use it, but it explains where fairness, the queue, and the hold count come from.

  • Why must lock() be called outside the try block rather than inside it?
    If lock() itself throws (rare, but possible, e.g. interruption with lockInterruptibly), and it sat inside the try, the finally would call unlock() on a lock this thread never acquired, throwing IllegalMonitorStateException and masking the real error. Acquiring outside the try guarantees finally/unlock only runs after a successful acquire.
  • If both are reentrant and comparable in speed, what concretely pushes you to ReentrantLock?
    A need for at least one of: a timeout on acquisition (tryLock with timeout), interruptible waiting (lockInterruptibly), a fairness guarantee, a non-blocking attempt (tryLock()), or multiple Condition wait-sets on one lock. None of these are expressible with synchronized.

saying these in an interview costs you the question

  • Claiming ReentrantLock is always faster than synchronized — they are comparable on modern JVMs; choose by features, not speed
  • Putting lock() inside the try block, so a failed acquire still triggers unlock()
  • Forgetting unlock() in finally, leaking the lock on exception
  • Saying synchronized is not reentrant — it is; reentrancy is not unique to ReentrantLock
  • Thinking 'reentrant' means re-entrant by *other* threads — it means the *same* thread can re-acquire

context