Definition
- Automatic release – when an exception is thrown inside a
synchronizedblock, the JVM releases the monitor lock before propagating the exception. This happens for all exceptions, errors, and runtime exceptions. - Bytecode mechanism – the Java compiler generates two
monitorexitinstructions: one at the end of the normal code path and one in an automatically generated exception handler. - synchronized advantage – because lock release is automatic, you cannot accidentally forget to release the lock. This is a significant safety advantage over
ReentrantLock. - ReentrantLock risk – with
ReentrantLock, you must calllock.unlock()in afinallyblock. If you forget and an exception is thrown, the lock is held permanently, causing all waiting threads to be deadlocked.
When a teller inside the vault (synchronized block) suddenly faints (exception), the vault door automatically unlocks. The vault has a built-in emergency mechanism (the second monitorexit) that opens the door regardless of what happens inside. With a manual padlock (ReentrantLock), if the teller faints while holding the key, nobody else can enter unless someone explicitly takes the key from the teller's hand (finally block calling unlock).
Understanding the Concepts
Does synchronized release the lock on exception?
Yes. The JVM guarantees that the monitor lock is released when the thread exits a synchronized block, whether normally or via an exception.
package com.bank.service;
import java.math.BigDecimal;
public class ExceptionReleaseDemo {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
synchronized (lock) {
System.out.println("T1: acquired lock");
throw new RuntimeException("Settlement failed!");
// Lock is released here despite the exception
}
} catch (RuntimeException e) {
System.out.println("T1: exception caught, lock released");
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("T2: acquired lock after T1's exception");
}
});
t1.start();
t1.join();
t2.start();
t2.join();
}
}
T1: acquired lock
T1: exception caught, lock released
T2: acquired lock after T1's exception
T2 successfully acquires the lock after T1's exception because the lock was automatically released.
How does the bytecode look?
You can verify this by running javap -c on the compiled class. The compiler generates two monitorexit instructions.
public void syncMethod();
Code:
0: aload_1
1: monitorenter // acquire lock
2: ... method body ...
8: aload_1
9: monitorexit // normal exit: release lock
10: goto 18
13: astore_2 // exception handler starts here
14: aload_1
15: monitorexit // exceptional exit: release lock
16: aload_2
17: athrow // rethrow the exception
18: return
The second monitorexit at instruction 15 is automatically generated by the compiler. It ensures the lock is released even when an exception is thrown.
How does this compare to ReentrantLock?
With ReentrantLock, you must manually release the lock in a finally block. Forgetting is a critical bug.
package com.bank.service;
import java.util.concurrent.locks.ReentrantLock;
public class LockComparisonDemo {
private final ReentrantLock lock = new ReentrantLock();
// CORRECT: unlock in finally
public void correctApproach() {
lock.lock();
try {
processSettlement(); // might throw!
} finally {
lock.unlock(); // always executed, even on exception
}
}
// WRONG: unlock never reached on exception
public void wrongApproach() {
lock.lock();
processSettlement(); // throws exception!
lock.unlock(); // never reached! Lock held forever!
}
private void processSettlement() {
throw new RuntimeException("DB connection failed");
}
}
In the wrong approach, when processSettlement() throws, lock.unlock() is never called. Every thread waiting for this lock is permanently deadlocked.
What are the edge cases?
The lock is released even for Error types like OutOfMemoryError and StackOverflowError. However, after such severe errors, the JVM state may be corrupted and continued operation is unreliable.
When should you prefer synchronized over ReentrantLock?
Prefer synchronized for simple mutual exclusion because it is safer (automatic lock release) and simpler (no try-finally boilerplate). Use ReentrantLock only when you need features that synchronized cannot provide: tryLock(), timeout, interruptibility, fairness, or multiple Condition objects.
What should a senior engineer know?
- Lock release on Error: the lock is released even on
OutOfMemoryError, but the application may be in an inconsistent state. In production, catchOutOfMemoryErrorat the top level and trigger a graceful shutdown. - Nested synchronized: if a thread holds multiple monitors and an exception is thrown, all monitors are released as the stack unwinds through each synchronized block.
- ReentrantLock hold count: if a thread calls
lock.lock()three times (reentrant), it must calllock.unlock()three times. An exception after the third lock but before all three unlocks leaves the lock in a partially held state unlessfinallyhandles it correctly.
Visual Overview
synchronized block bytecode:
monitorenter ──▶ method body ──▶ monitorexit (normal)
│ │
│ exception │
└──────────▶ monitorexit ──────▶ athrow
(exceptional)
ReentrantLock (must be manual):
lock.lock() ──▶ try { body } ──▶ finally { lock.unlock() }
│ │
│ exception │
└──────────▶ finally ──────────▶ lock.unlock()
(developer must write this!)
Key Takeaway
When an exception is thrown inside a synchronized block, the JVM automatically releases the monitor lock. The compiler generates two monitorexit instructions in the bytecode: one for the normal exit path and one for the exceptional exit path. This makes synchronized blocks inherently safe: you cannot accidentally forget to release the lock.
This is a significant advantage over ReentrantLock, where lock.unlock() must be explicitly placed in a finally block. Forgetting this causes a permanent lock hold: every other thread waiting for the lock is deadlocked until the JVM is restarted.
Remember one sentence: synchronized always releases the lock on exception because the JVM generates a second monitorexit on the exceptional path, while ReentrantLock requires a manual finally block.
---