skip to content

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