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 synchronized keyword 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 ReentrantLock and call lock() / 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

Java
public 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

Java
ReentrantLock 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

Java
ReentrantLock 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

FeaturesynchronizedReentrantLock
SyntaxKeyword, compactExplicit lock/unlock in finally
tryLock / timeoutNoYes (tryLock, tryLock(time))
Fair lockNoYes (constructor param)
Multiple conditionsNo (one wait set)Yes (newCondition())
Lock status (e.g. isHeldByCurrentThread)NoYes
DefaultBuilt-in, good for simple casesWhen 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.