ReentrantLock gives the same mutual exclusion and reentrancy as synchronized, but adds capabilities the keyword cannot offer: interruptible and timed acquisition, non-blocking tryLock, optional fairness, multiple Condition wait-sets, and the ability to lock and unlock across method boundaries.
What ReentrantLock adds
- Timed / interruptible acquisition —
tryLock(timeout, unit)andlockInterruptibly(). A thread blocked onsynchronizedcannot be interrupted; one blocked onlockInterruptibly()can. - Non-blocking attempt —
tryLock()returns immediately withtrue/false, enabling back-off strategies that avoid deadlock. - Fairness —
new ReentrantLock(true)grants the lock in FIFO order.synchronizedis always unfair. - Multiple conditions —
newCondition()lets you have separate "not full" and "not empty" wait-sets on one lock; a monitor has exactly one. - Introspection —
isLocked(),getHoldCount(),hasQueuedThreads().
The cost: you own the unlock
synchronized releases the monitor automatically on scope exit. An explicit lock does not — you must release it yourself, always in finally:
private final ReentrantLock lock = new ReentrantLock();
public void transfer(Account to, long amount) {
lock.lock(); // acquire (reentrant: nested lock() is fine)
try {
balance -= amount;
to.deposit(amount);
} finally {
lock.unlock(); // MUST run even if the body throws
}
}
Both are reentrant: the same thread may re-acquire the lock it already holds (getHoldCount() increments), and must unlock() the same number of times.
When to use which
Prefer synchronized by default — it is less error-prone (no leaked locks), reads cleanly, and the JVM optimizes it well (lock coarsening, biased locking history). Reach for ReentrantLock only when you need one of its extra features. Both provide identical happens-before/visibility guarantees, so there is no memory-model reason to switch.