skip to content

ReentrantLock & Conditions

ReentrantLock trades automatic release for real capabilities: tryLock with a timeout, lockInterruptibly, optional fairness, and multiple Condition wait-sets. Interviewers ask when it beats synchronized, and expect to hear unlock in a finally block.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

5

What does tryLock() do, and how do its non-blocking and timed forms help avoid deadlock?

level: middleimportance: must knowfreq 70%

answer

  1. tryLock() = non-blocking, returns true/false instantly
  2. tryLock(time, unit) = bounded wait, then give up
  3. Deadlock avoidance: try-all, release-all-on-failure, retry
  4. Add randomized backoff to dodge livelock
  5. Only unlock what tryLock returned true for; no-arg tryLock barges fairness

basics

~20 s

tryLock() tries to grab the lock and returns true or false right away instead of waiting. tryLock(time, unit) waits only up to a time limit. This lets a thread give up instead of blocking forever, so threads can back off and avoid getting stuck in a deadlock.

solid answer

~50 s

tryLock() attempts to acquire the lock and returns immediately: true if it got it, false if another thread holds it — it never blocks. tryLock(timeout, unit) blocks only up to the given time, then returns false. Both let a thread take an alternative path instead of waiting indefinitely, which is impossible with synchronized. The deadlock-avoidance pattern is acquiring multiple locks: instead of blocking on a second lock while holding a first (the setup for a deadlock when another thread grabs them in the opposite order), each thread tries the locks, and if any tryLock fails it releases everything it holds and retries after a short randomized backoff. Crucially, only unlock a lock when its tryLock returned true, and always release in finally. The trade-off is that you get a livelock risk and must write the retry loop yourself.

code

java · 14 lines
java
boolean transfer(Account a, Account b, long amt) throws InterruptedException {
    if (a.lock.tryLock(200, TimeUnit.MILLISECONDS)) {
        try {
            if (b.lock.tryLock(200, TimeUnit.MILLISECONDS)) {
                try {
                    a.balance -= amt;
                    b.balance += amt;
                    return true;            // got both locks
                } finally { b.lock.unlock(); }
            }
        } finally { a.lock.unlock(); }      // release a if b failed
    }
    return false;                            // backed off; caller may retry
}

go deeper

for a junior

Understands tryLock() returns true/false without blocking and the timed form gives up after a deadline.

for a middle

Can write the lock/try/finally with tryLock correctly, unlocking only on success, and explains how giving up avoids waiting forever.

for a senior

Builds the try-all/release-all/retry deadlock-avoidance pattern, recognizes the resulting livelock, and applies randomized backoff; knows no-arg tryLock barges fairness.

for a principal

Weighs global lock ordering vs tryLock retries at design scale, reasons about the four Coffman deadlock conditions, sets timeouts/backoff policy, and considers lock-free or higher-level alternatives to reduce multi-lock coupling.

## Background: blocking and deadlock A **lock** lets only one **thread** (concurrent execution path) into a guarded section at a time. The classic acquisition methods *block*: a thread that requests a held lock is suspended until the lock is free. Blocking is fine — until two threads block on each other. A **deadlock** is a permanent standstill: thread A holds lock 1 and waits for lock 2, while thread B holds lock 2 and waits for lock 1. Neither can proceed, neither will release, and both are stuck forever. The root cause here is **lock-ordering inconsistency** — different threads acquire the same locks in different orders. `synchronized` offers no escape: once it starts waiting for a monitor, it waits forever. `ReentrantLock` adds two methods that let a thread *decline to wait*. ## tryLock() — the non-blocking attempt ```java if (lock.tryLock()) { // returns immediately try { // got it — do the work } finally { lock.unlock(); // only unlock because tryLock returned true } } else { // didn't get it — do something else, skip, or retry later } ``` `tryLock()` acquires the lock **only if it is free at that instant**, returning `true`; otherwise it returns `false` instantly without blocking. The thread keeps running and can choose an alternative. (Caveat: the no-arg `tryLock()` **barges** ahead of any fairness queue — it grabs a free lock even if other threads have been waiting longer. If you set fairness, use the timed `tryLock(0, unit)` form to honor the queue.) ## tryLock(timeout, unit) — the bounded wait ```java if (lock.tryLock(500, TimeUnit.MILLISECONDS)) { try { /* work */ } finally { lock.unlock(); } } else { // gave up after 500ms } ``` This blocks like `lock()` but only up to the timeout, then returns `false`. It also throws `InterruptedException` if interrupted while waiting, so it is both *timed* and *interruptible*. Use it to bound how long an operation can stall — essential in latency-sensitive or responsive systems. ## Deadlock avoidance: the try-and-back-off pattern When you must hold two locks at once and cannot guarantee a global lock ordering, use `tryLock` to break the deadlock condition: a thread never blocks while holding another lock. Instead it *tries* each, and on any failure releases everything and retries. ```java while (true) { if (lock1.tryLock()) { try { if (lock2.tryLock()) { try { doWork(); return; // success — both held } finally { lock2.unlock(); } } } finally { lock1.unlock(); } // release lock1 if lock2 failed } Thread.sleep(random.nextInt(50)); // randomized backoff before retry } ``` Because no thread ever *waits* while holding a lock, the circular-wait condition required for deadlock cannot form. ## The new risk: livelock If two threads repeatedly grab one lock, fail the other, release, and retry **in lockstep**, they can spin forever making no progress — a **livelock** (the threads aren't blocked, they're just endlessly undoing each other). The fix is the **randomized backoff** above: random sleep before retry desynchronizes them so one wins. ## Rules to get right 1. **Only `unlock()` a lock whose `tryLock()` returned `true`.** Unlocking a lock you never acquired throws `IllegalMonitorStateException`. 2. **Still use `finally`** for every successful acquisition. 3. Prefer a **consistent global lock order** when you can arrange one — it's simpler and deadlock-free without retries; reserve `tryLock` for when ordering is impossible (e.g. locks chosen at runtime).

  • tryLock enables deadlock avoidance, but introduces livelock. How do you prevent the livelock?
    Add a randomized backoff (a short random sleep) between retries so the competing threads stop retrying in perfect lockstep; one then wins the race and proceeds. Optionally cap the retries and escalate/abort after N attempts.
  • When would a consistent global lock ordering be preferable to the tryLock retry pattern?
    Whenever the set of locks is known and you can always acquire them in the same total order — that statically eliminates the circular-wait condition with simpler blocking code and no retries or livelock. Reserve tryLock for cases where lock identity is only known at runtime and a fixed order can't be imposed.

saying these in an interview costs you the question

  • Unlocking a lock when tryLock returned false (IllegalMonitorStateException)
  • Retrying immediately with no backoff, causing livelock
  • Believing tryLock prevents all deadlocks automatically — it enables a pattern; you must write release-and-retry
  • Assuming no-arg tryLock() respects fairness — it barges; use tryLock(0, unit) to honor the queue
  • Confusing deadlock (everyone blocked) with livelock (everyone busy but no progress)

context

open as a page

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

level: middleimportance: must knowfreq 78%

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.

open as a page

What is a Condition obtained from a Lock, and why can it be better than wait()/notify() on a monitor?

level: seniorimportance: must knowfreq 62%

basics

~20 s

A Condition is a waiting area tied to a Lock, created with lock.newCondition(). Threads call await() to wait and signal()/signalAll() to wake them — like wait()/notify() but you can have several separate waiting areas per lock, so you can wake exactly the right group of waiters.

open as a page

What is the fairness option on ReentrantLock, and what is the trade-off of enabling it?

level: seniorimportance: should knowfreq 48%

basics

~20 s

A fair ReentrantLock (new ReentrantLock(true)) hands the lock to waiting threads in the order they asked for it, so no thread is starved. The default unfair lock lets whoever grabs it first win, which is faster overall but can leave some threads waiting longer.

open as a page

What is the difference between lock(), lockInterruptibly(), and tryLock() with respect to interruption and blocking?

level: seniorimportance: should knowfreq 44%

basics

~20 s

lock() waits for the lock no matter what and ignores interruption while waiting. lockInterruptibly() waits but throws InterruptedException if the thread is interrupted, so the wait can be cancelled. tryLock() doesn't wait at all (or only up to a timeout) and returns true/false.

open as a page