synchronized vs ReentrantLock
Both synchronized and ReentrantLock provide mutual exclusion and reentrancy in Java. synchronized is built-in and concise; ReentrantLock is explicit and offers more features (tryLock, fair lock, multiple conditions, lock status). This article compares them and gives when to use which, with examples and a short table.
Overview
- synchronized: Use the
synchronizedkeyword on a method or block; the lock is the object monitor. Reentrant: same thread can enter again. No explicit unlock (block exit or return does it). No tryLock, no fairness option, one condition per monitor. - ReentrantLock: Create a
ReentrantLockand calllock()/unlock(). Must unlock in finally to avoid leaking the lock. Supports tryLock (with or without timeout), fair lock, and multiple Condition instances. Built on AQS. - When to use which: Prefer synchronized for simple cases (one lock, no need for tryLock or fairness). Use ReentrantLock when you need tryLock, timeout, fairness, or multiple conditions, or when you want to hold the lock across a non-blocking structure (e.g. hand-over-hand locking).
Example
Example 1: synchronized — simple and concise
Javapublic synchronized void increment() { count++; } // or public void increment() { synchronized (lock) { count++; } }
- No explicit unlock; reentrant by default. Good when you do not need tryLock or fairness.
Example 2: ReentrantLock with tryLock
JavaReentrantLock lock = new ReentrantLock(); if (lock.tryLock(1, TimeUnit.SECONDS)) { try { doWork(); } finally { lock.unlock(); } } else { // alternative path: don't block }
- tryLock avoids blocking forever; useful for timeouts and “do something else if lock is busy.” Must unlock in finally.
Example 3: ReentrantLock with Condition
JavaReentrantLock lock = new ReentrantLock(); Condition notFull = lock.newCondition(); Condition notEmpty = lock.newCondition(); // One lock, two conditions (e.g. bounded queue: wait on notFull when full, wait on notEmpty when empty) lock.lock(); try { while (count == capacity) notFull.await(); // ... notEmpty.signal(); } finally { lock.unlock(); }
- synchronized has one wait set (wait/notify); ReentrantLock can have multiple Conditions so you can signal “not full” vs “not empty” separately. Useful for producer-consumer.
Example 4: Comparison table
| Feature | synchronized | ReentrantLock |
|---|---|---|
| Syntax | Keyword, compact | Explicit lock/unlock in finally |
| tryLock / timeout | No | Yes (tryLock, tryLock(time)) |
| Fair lock | No | Yes (constructor param) |
| Multiple conditions | No (one wait set) | Yes (newCondition()) |
| Lock status (e.g. isHeldByCurrentThread) | No | Yes |
| Default | Built-in, good for simple cases | When you need extra control |
Core Mechanism / Behavior
- synchronized: JVM uses monitorenter/monitorexit (or equivalent); the object header holds the lock state. Reentrancy is tracked in the monitor. No way to “try” or “wait with timeout” at the language level.
- ReentrantLock: Implemented with AQS; state = 0 (free) or 1+ (held, reentrant count). tryLock uses tryAcquire; fair lock uses queue order. Condition uses a separate wait queue per condition.
- Performance: In modern JVMs, synchronized is heavily optimized (biased locking, etc.); for typical use they are comparable. ReentrantLock can be faster under high contention when you use tryLock and avoid blocking.
Key Rules
- Prefer synchronized when you only need mutual exclusion and reentrancy; it is simpler and harder to misuse (no forgotten unlock). Use ReentrantLock when you need tryLock, timeout, fairness, or multiple conditions.
- Always call unlock() in a finally block when using ReentrantLock so that an exception does not leave the lock held. Prefer lock() immediately before try { ... } finally { unlock(); }.
- Use the same lock type consistently in a given component; mixing synchronized and ReentrantLock on the “same” logical lock is error-prone (they are different locks).
What's Next
See AQS Explained for how ReentrantLock is built. See Deadlock Detect and Prevent for lock ordering and tryLock. See Volatile and JMM for visibility when sharing state.