The 30 second answer
Both give mutual exclusion and the same memory visibility guarantees. Synchronized is simpler and always releases the lock when the block ends, even on an exception. ReentrantLock gives more control: try to lock with a timeout, wait in a way that can be interrupted, optional fairness, and multiple conditions. The cost is that I must unlock in a finally block myself.
private final ReentrantLock lock = new ReentrantLock();
void transfer() throws InterruptedException {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try { /* critical section */ }
finally { lock.unlock(); }
} else {
// back off instead of waiting forever
}
}| synchronized | ReentrantLock | |
|---|---|---|
| Release | automatic | manual, in finally |
| Try / timeout | no | tryLock(), tryLock(timeout) |
| Interruptible wait | no | lockInterruptibly() |
| Fairness | no | new ReentrantLock(true) |
| Conditions | one wait set (wait / notify) | many Conditions (await / signal) |
Both are reentrant: the owning thread can acquire the lock again. [Opinion] Default to synchronized; use ReentrantLock when you need its extra features. On Java 21 with virtual threads, also prefer ReentrantLock around long blocking calls to avoid pinning (fixed for synchronized in Java 24 by JEP 491).
Sources: ReentrantLock Javadoc, Lock Javadoc (memory synchronization), JLS 17.1 Synchronization, JEP 444: Virtual Threads