skip to content

How should finally be used to release a lock or restore state, and what reliability limits should you keep in mind?

level: principalimportance: should knowfreq 48%

answer

  1. lock() OUTSIDE try, unlock() in finally
  2. Acquire-inside-try bug: unlock a lock never held
  3. finally covers success AND failure (catch only failure)
  4. finally is in-process best-effort; skipped on exit/crash
  5. Distributed locks need leases/TTL, not just finally

basics

~20 s

Acquire the lock before the try, then unlock in finally so it is always released even on error. But remember finally is skipped on System.exit or a crash, so do not rely on it alone for state that must survive process death.

solid answer

~50 s

The standard lock idiom is: call lock() outside the try, then immediately enter try and put unlock() in finally. This guarantees the lock is released on every normal and exception path, preventing deadlocks from leaked locks. Acquiring inside the try is a bug, because if lock() throws you would unlock a lock you never held. The same finally pattern restores any in-memory invariant: a thread-local, a feature flag, a mutated counter. The reliability caveat at scale: finally only protects against in-process exceptions and early jumps. It does not run on System.exit, Runtime.halt, JVM crash, kill -9, or hardware failure. So for invariants that must hold across process death (durable state, external resources, distributed locks), finally is necessary but not sufficient; you also need timeouts/leases, shutdown hooks, idempotent recovery, or transactional/WAL semantics. Treat finally as best-effort in-process cleanup, layered with crash-safe mechanisms.

go deeper

for a junior

Knows to put unlock() in finally so the lock is always released.

for a middle

Uses the acquire-before-try, unlock-in-finally idiom and can explain why catch alone would leak the lock.

for a senior

Explains the IllegalMonitorStateException risk of acquiring inside try and applies the pattern to general state restoration.

for a principal

Reasons about finally's in-process best-effort boundary versus crash-safe needs, choosing leases, idempotency, transactions, or shutdown hooks for distributed/durable invariants.

## The lock idiom The canonical pattern for an explicit lock (e.g. java.util.concurrent.locks.ReentrantLock): ```java lock.lock(); // OUTSIDE the try try { // critical section } finally { lock.unlock(); // always releases, even on exception/return } ``` Why lock() goes *outside* the try: `unlock()` of a lock you do not hold throws `IllegalMonitorStateException`. If you wrote `try { lock.lock(); ... } finally { lock.unlock(); }` and `lock()` itself threw (or was interrupted), finally would attempt to unlock a never-held lock, masking the real error. Acquiring before try means: if acquisition fails, you never enter the try, so finally never runs - which is exactly right. This generalizes to any state restoration: ```java boolean prev = flag; flag = true; try { // work that assumes flag == true } finally { flag = prev; // restore invariant regardless of outcome } ``` ## Why finally and not catch Cleanup must happen on **both** success and failure. catch only fires on failure; the normal path would leak the lock. finally is the single place that covers every exit, so it is the correct home for release/restore. ## The reliability boundary (the principal-level point) finally's guarantee is **in-process and best-effort**. It runs only if the thread is alive and control leaves the try. It is skipped by: - `System.exit(int)` / `Runtime.getRuntime().halt(int)` - JVM crash (fatal native error, unrecoverable Error bringing down the process) - `kill -9`, power loss, container OOM-kill - abrupt thread termination Consequences for design: - An **in-memory** lock released in finally is fine for in-process safety; if the process dies, the lock dies with it, so there is nothing to leak. Good. - A **distributed lock** (Redis, ZooKeeper, a DB row) released in finally is NOT safe across process death: if the JVM is killed after acquiring but before finally, the remote lock leaks. The fix is a **lease/TTL** (the lock auto-expires) plus idempotent re-acquisition, not finally alone. - **Durable external state** (a file half-written, a temp resource, an audit record) that must be consistent across a crash needs transactions, write-ahead logging, or crash-recovery cleanup - finally cannot promise it. ## Shutdown hooks vs finally `Runtime.addShutdownHook` runs on `System.exit` and normal JVM shutdown (but not on `halt` or kill -9). It is a coarse, process-wide net for flushing/closing, complementary to finally - not a replacement, because it cannot see per-try-block local state and does not run on every termination. ## Summary mental model Use finally for deterministic in-process cleanup (locks, flags, state restoration, with the acquire-before-try rule). Layer crash-safe mechanisms (leases, idempotency, transactions, shutdown hooks) for anything that must survive the process itself ending.

  • Why must lock() be called outside the try block?
    If lock() were inside try and it threw, finally would call unlock() on a lock you never acquired, throwing IllegalMonitorStateException and masking the original failure. Acquiring before try means finally only runs when the lock is held.
  • finally releases an in-process lock fine, but why is it insufficient for a distributed lock?
    If the JVM is killed (System.exit, kill -9, crash) after acquiring the remote lock but before finally runs, the remote lock leaks. You need a TTL/lease and idempotent recovery, not just finally.

saying these in an interview costs you the question

  • Putting lock() inside the try block
  • Relying on finally to release a distributed lock across crashes
  • Assuming shutdown hooks fully replace finally (they miss halt/kill -9)
  • Using catch instead of finally for release, leaking the lock on success

context