Last Updated: February 5, 2026
Imagine two database connections, each holding one lock and waiting for the other. Connection A holds the "users" table lock and waits for the "orders" table lock. Connection B holds the "orders" table lock and waits for the "users" table lock. Both wait forever.
This is the classic deadlock, and it happens because each connection commits to waiting indefinitely for a resource that will never become available.
What if, instead, a connection could say: "I'll try to get this lock, but if I can't get it within 100 milliseconds, I'll back off, release what I have, and try a different approach"?
This is exactly what try-lock and timed locking provide. Instead of blocking forever, threads can attempt to acquire a lock without waiting (try-lock), or wait only for a limited time (timed lock). If acquisition fails, the thread can release its other locks, back off, and retry.
This chapter covers how these mechanisms work internally, when to use them, and the patterns that make deadlock-free systems possible.
Try-lock is a non-blocking lock acquisition attempt. If the lock is available, it's acquired and the method returns true. If the lock is held by another thread, it returns false immediately without waiting. The thread isn't blocked at all; it gets an instant answer about whether it succeeded.
tryLock() has a simple two-step flow: check if the lock is available, and either acquire it (green path) or return false immediately (red path). There's no waiting involved, making it completely non-blocking.
Timed lock is similar but adds patience. It waits up to a specified timeout for the lock to become available. If the lock becomes available within the timeout, it's acquired and the method returns true. If the timeout expires before the lock becomes available, it gives up and returns false.
tryLock(timeout) adds an additional waiting phase (orange). When the lock isn't available, the thread enters a wait state. During this wait, if the lock becomes available (perhaps because another thread released it), the waiting thread acquires it and returns true. But if the timeout expires first, the thread gives up and returns false.
Understanding when to use each locking method is crucial. Here's how they compare:
| Operation | Behavior on Contention | Use Case |
|---|---|---|
lock() | Blocks indefinitely until acquired | Simple critical sections where waiting is acceptable |
tryLock() | Returns false immediately | Polling, deadlock avoidance, non-blocking algorithms |
tryLock(timeout) | Waits up to timeout, then returns false | Responsive systems, bounded latency requirements |
The regular lock() method is like standing in line at the bank: you commit to waiting however long it takes. tryLock() is like glancing at the line and deciding immediately whether to wait or come back later. tryLock(timeout) is like deciding to wait but setting a timer on your phone: if you're not served in 10 minutes, you leave.
Try-lock isn't just a convenience feature; it's a fundamental tool for building robust concurrent systems. Let's explore the three main scenarios where it becomes essential.
The primary use case for try-lock is avoiding deadlock when acquiring multiple locks. Without try-lock, the traditional solution is global lock ordering: all threads must acquire locks in the same predetermined order. This works but requires coordination across all code that uses the locks, which becomes impractical in large systems or when using third-party libraries.
Try-lock provides an alternative: threads can back off and retry. Let's see this with a concrete example. The first sequence diagram shows what happens without try-lock:
Thread A successfully acquires Lock 1, while Thread B successfully acquires Lock 2. Then Thread A tries to acquire Lock 2, which is held by Thread B, so Thread A blocks. Meanwhile, Thread B tries to acquire Lock 1, which is held by Thread A, so Thread B blocks. Neither can proceed because each is waiting for the other. This is the deadlock.
Now let's see how try-lock changes the outcome:
The key difference is Thread A's response when tryLock() fails on Lock 2. Instead of waiting forever, Thread A immediately learns that Lock 2 isn't available. It then releases Lock 1 (breaking the circular dependency), backs off with a random sleep (to avoid immediately colliding again), and retries.
Meanwhile, Thread B attempts to acquire Lock 1 with tryLock() and succeeds because Thread A released it. Thread B now has both locks, completes its work, and releases both. When Thread A retries after its backoff period, both locks are now available.
This pattern breaks the "hold and wait" condition that's necessary for deadlock. Threads don't hold one lock while indefinitely waiting for another; they release and retry.
In systems where responsiveness matters, blocking forever is unacceptable. Consider a web service handler: if a request handler blocks waiting for a lock held by a slow operation, the request times out, the user sees an error, and system resources are tied up. Timed locks provide bounded latency.
This handler guarantees a response within roughly 100 milliseconds (plus processing time). If the lock is contended and can't be acquired within that window, the handler returns a graceful "service unavailable" response rather than hanging. The client can retry, and the server thread is freed to handle other requests.
This pattern is essential for:
Understanding the internal mechanics helps you reason about behavior in edge cases and implement try-lock correctly.
The following state diagram shows how a lock transitions between states when different operations are called:
Let's trace through the important transitions:
lock() or tryLock() succeeds, the lock moves to the Locked state. The green "Unlocked" state represents availability.tryLock() on an already-locked lock, the lock stays in the Locked state (nothing changes), but the calling thread receives false.tryLock(timeout) on a locked lock, that thread enters a waiting state (orange). From here, two outcomes are possible: either the lock becomes available before the timeout (transition to Locked, representing successful acquisition), or the timeout expires (transition to Failed, red, representing giving up).Note that the lock's state doesn't actually change to "TimedWait." The lock is still Locked. The TimedWait state represents the waiting thread's perspective, not the lock's state.
The following flowchart shows the detailed decision process when a thread calls tryLock():
Walking through this flowchart:
true. The green path indicates success.true.false (red path). No waiting, no blocking, just an instant answer.The key property of tryLock() is that it never blocks. Regardless of the lock's state, the call returns immediately. This is what makes it useful for deadlock avoidance: you can check if a lock is available without committing to waiting.
The timed variant adds a waiting phase:
The difference from tryLock() is the orange waiting phase:
false.The loop between "Signaled" and "Re-check lock state" is important. In a system with multiple waiters, when the lock is released, all waiters might be signaled, but only one can acquire. The others must either wait again (if time remains) or give up.
Now let's see how try-lock translates to actual code in different languages. The concepts are the same, but the APIs differ.
Let's examine the key patterns in this code:
Non-blocking `tryIncrement()`: The method attempts to acquire the lock with tryLock(). If successful (returns true), we enter the try-finally block where we do the actual work and guarantee the lock is released. If tryLock() returns false, we skip the critical section entirely and return false to the caller.
Timed `tryIncrementWithTimeout()`: Similar structure, but using tryLock(timeout, unit). This version will wait up to timeoutMs milliseconds for the lock. Note that this method throws InterruptedException because the waiting can be interrupted.
The try-finally pattern: Always use try-finally when working with explicit locks. If the code inside the critical section throws an exception, the finally block ensures the lock is released. Without this, an exception would leave the lock held forever.
The classic example of deadlock avoidance with try-lock is transferring money between two bank accounts. Without proper handling, transferring from A to B while simultaneously transferring from B to A creates a deadlock if both threads try to acquire locks in opposite order.
Let's trace through the key aspects of this implementation:
The retry loop: The outer while loop continues until we either succeed or the overall timeout expires. This provides bounded execution time even if we keep failing to acquire both locks.
Nested try-lock: We first try to acquire the from account's lock. Only if that succeeds do we try to acquire the to account's lock. This nesting is important: we don't want to hold from's lock while spinning on to.
Release and retry: If we can't get the second lock, the finally block releases the first lock. We don't stay holding the first lock while waiting. This is what breaks the deadlock potential.
Random backoff: The Thread.sleep(random.nextInt(10) + 1) adds a random delay between 1 and 10 milliseconds before retrying. This randomness is crucial. Without it, two threads might keep colliding in perfect synchronization, each acquiring one lock and failing on the other, forever. The randomness ensures that eventually one thread backs off longer than the other, breaking the symmetry.
Money conservation: At the end of the demo, Alice and Bob's balances might have changed, but the total should still be 2000. No money is lost or created because every transfer that executes either fully completes or fully fails.
Understanding performance characteristics helps you choose appropriate timeouts and decide between locking strategies.
tryLock() usually succeeds immediately. Its fast path is very efficient, just an atomic compare-and-swap.tryLock() has a high success rate even under moderate contention.tryLock() lets you avoid wasting time waiting.tryLock() calls fail. The overhead of failed attempts and retries can exceed the cost of just waiting.| Operation | Typical Time (uncontended) |
|---|---|
lock() (uncontended) | ~20ns |
tryLock() (success) | ~20ns |
tryLock() (failure) | ~5ns (fast fail) |
tryLock(timeout) (success) | ~20ns |
tryLock(timeout) (timeout) | timeout duration + overhead |
The key insight is that tryLock() failure is very fast (~5ns) because it just checks the state and returns without any waiting or queue operations. This makes it viable for polling patterns.
Consider these factors when choosing timeout values:
Rule of thumb formula:
For example, with a lock held ~1ms, ~5 contending threads, and 3x safety:
For responsive systems, you might cap this at your SLA requirement (e.g., 50ms max latency) regardless of the formula result.
What does tryLock() return when the lock is already held by another thread?