What do a non-blocking lock attempt (tryLock) and a timed lock acquisition give you that a plain blocking acquire does not, and what goes wrong when they are used carelessly?
answer
- bounded wait, two outcomes
- breaks hold-and-wait
- release everything, back off, retry
- randomised jitter or livelock
- empty else branch = silent no-op
basics
~20 sThey bound how long you wait: the attempt returns success or failure instead of blocking indefinitely, so you can take a fallback path, stay responsive, or release locks you already hold and retry. The risks are livelock from lockstep retries and code that ignores the failure branch.
solid answer
~50 sA plain acquire is an unbounded commitment: you wait as long as it takes, and while waiting you keep every lock you already hold. `tryLock` returns immediately with a boolean; timed acquisition waits at most a deadline. Both convert 'block forever' into a branch you must handle. That branch buys three things. **Deadlock avoidance** where a strict global lock ordering is impossible: acquire the second lock with a try, and on failure release the first, back off, and retry — no cycle can persist. **Responsiveness**: a request handler can fail fast, shed load, or answer from a cache rather than pile up on a contended lock. **Observability**: counting failed attempts is a direct contention metric. The hazards: retry loops that resynchronise and livelock, so back off with randomised jitter and a bounded retry count; failure paths that are unwritten or untested; treating tryLock in a tight loop as a spinlock; and, on a fair lock, a barging tryLock that jumps the queue.
code
text · 10 linesretries = 0
loop:
acquire(A)
if tryAcquire(B, 10ms):
try { transfer() } finally { release(B); release(A) }
return OK
release(A) // never hold-and-wait
retries += 1
if retries > MAX: return BUSY // bounded, not forever
sleep(random(0, base * 2^retries))go deeper
Know that try returns a boolean and never blocks, that timed acquire waits at most a deadline, and that you must handle the failure case.
Explain the release-and-retry pattern for two locks and why the first lock must be released before retrying.
Discuss livelock and randomised backoff, bounded retries, treating timed acquisition as load shedding, and using failure counts as a contention metric.
Treat it as a latency-versus-success-rate policy: decide where the system prefers a bounded error rate over an unbounded tail, and prefer a global lock ordering over try-and-retry wherever a stable order exists.
## The semantics Three acquisition modes matter: | Mode | Blocks? | Returns | |---|---|---| | `acquire()` | until granted | nothing — you hold it | | `tryAcquire()` | never | true (held) / false (not held) | | `tryAcquire(timeout)` | up to the deadline | true / false | The crucial change is control flow: a blocking acquire has one outcome, so callers write no failure logic. The `try` variants have two, and the second one is a design decision — retry, degrade, fail the request, or escalate. ## Use 1 — breaking potential deadlock cycles Deadlock needs four conditions to hold at once; one of them is **hold-and-wait**. A blocking acquire while already holding a lock is hold-and-wait made concrete. A timed or non-blocking attempt breaks it: ``` acquire(A) if (not tryAcquire(B)): release(A) // give up everything sleep(randomBackoff()) // desynchronise retry ``` This is the standard technique when a consistent global lock order cannot be established — for example transferring between two accounts identified only at runtime, where the natural order differs per call. Ordering locks by a stable key is still the better fix when available; try-and-back-off is what you use when it is not. ## Use 2 — bounded latency and load shedding In a request-serving system, an unbounded acquire converts lock contention into unbounded queueing: threads pile up invisibly, the latency tail explodes, and thread pools saturate. A timed acquire converts it into an explicit, measurable failure: return a cached value, return 'busy', or route elsewhere. That is a deliberate choice to trade a small error rate for a bounded p99. ## Use 3 — cancellation and health A timed acquire gives cancellation points: a shutdown routine that must not hang, a watchdog that reports 'lock held longer than X', or a diagnostic that samples how often acquisition fails. Failed-attempt counts and wait-time histograms are among the cheapest contention signals you can collect. ## The failure modes **Livelock.** Threads that fail, immediately retry, fail again, and repeat make progress-free noise. Because they retry in lockstep after each collision, they can collide indefinitely. The fix is randomised exponential backoff, exactly as in network collision avoidance: sleep a random interval that grows with the retry count. Without randomisation, symmetric threads resynchronise; without a bound, a starving thread never gives up. **Unwritten failure paths.** `if (tryLock()) { ...work... }` with no `else` silently skips the work. This is a real and common bug: the code compiles, tests pass under no contention, and under load the operation quietly does not happen. Every try must have a considered alternative — including 'fall back to a blocking acquire with a deadline'. **Try-as-spin.** A tight `while (!tryLock()) {}` loop burns a core and, on a single-core or oversubscribed system, can prevent the holder from being scheduled to release it. Spinning is a legitimate strategy but with a bounded spin count then a park, which is what a proper adaptive lock already does internally. **Fairness interaction.** On a fair lock, plain acquire queues, but an untimed `tryAcquire` typically succeeds only if the lock is *free*, and in some designs it barges ahead of queued waiters. Mixing barging tries into a fair lock quietly reintroduces the starvation that fairness was chosen to prevent. **Correct release discipline.** The acquire result must gate the release: releasing a lock you failed to acquire is an error, and forgetting to release on the success path is a leak. The safe shape is: attempt, and only on success enter a try/finally that releases.
- Your try-and-retry loop is spinning at 100% CPU and no transfer completes. What is happening and how do you fix it?That is livelock: threads keep failing, retrying in lockstep, and colliding again, so they are busy but make no progress. Add randomised exponential backoff so competing threads desynchronise, cap the retry count so a caller eventually returns an error rather than looping, and consider imposing a stable global lock order so the try path is rarely needed at all.
saying these in an interview costs you the question
- Writes tryLock with no else branch, silently skipping the work
- Retries immediately in a tight loop with no backoff or jitter
- Keeps the first lock held while blocking for the second
- Releases the lock even when the attempt returned false
- Claims tryLock 'prevents deadlock' by itself, without releasing held locks