Q35 of 40 · Core Java

Explain the synchronized keyword vs ReentrantLock — when would you choose each?

Core JavaSeniorsynchronizedreentrantlockconcurrencylocksthreadingjava

Short answer

Short answer: synchronized is the built-in JVM monitor mechanism: simple, guaranteed to be released on exception, but limited to block-scoped locking. ReentrantLock from java.util.concurrent.locks offers the same mutual exclusion but adds tryLock (non-blocking acquire), lockInterruptibly, fairness policies, and separate Condition objects for fine-grained wait/notify. Prefer synchronized unless you need one of those advanced features.

Detail

synchronized

// Method-level — locks on 'this'
public synchronized void addUser(String name) {
    users.add(name);
}

// Block-level — explicit lock object, finer granularity
private final Object lock = new Object();

public void transfer(Account from, Account to, int amount) {
    synchronized (lock) {
        from.debit(amount);
        to.credit(amount);
    } // lock always released, even on exception
}

ReentrantLock

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

private final ReentrantLock lock = new ReentrantLock(true); // fair

// tryLock — don't wait if lock unavailable
public boolean tryTransfer(Account from, Account to, int amount)
        throws InterruptedException {
    if (!lock.tryLock(100, TimeUnit.MILLISECONDS)) {
        return false; // could not acquire, report failure
    }
    try {
        from.debit(amount);
        to.credit(amount);
        return true;
    } finally {
        lock.unlock(); // MUST be in finally — ReentrantLock doesn't auto-release
    }
}

// lockInterruptibly — abort if thread is interrupted while waiting
public void workUntilInterrupted() throws InterruptedException {
    lock.lockInterruptibly();
    try {
        // ... work
    } finally {
        lock.unlock();
    }
}

Feature comparison

Feature synchronized ReentrantLock
Auto-release on exception Yes No — must use finally
tryLock (non-blocking) No Yes
Timed lock attempt No Yes
Interruptible lock wait No Yes
Fairness option No Yes (constructor arg)
Multiple Condition objects No (one per monitor) Yes
Reentrancy Yes Yes

When to use ReentrantLock

  • Need tryLock to avoid deadlock or implement timeout-based acquisition
  • Need lockInterruptibly for responsive shutdown
  • Need multiple Condition variables (producer/consumer with separate queues)
  • Need fairness to prevent starvation

Otherwise, synchronized is simpler, less error-prone (no missed unlock), and the JVM optimises it well.

// WHAT INTERVIEWERS LOOK FOR

That synchronized auto-releases on exception but ReentrantLock requires a finally block. The tryLock use case. When fairness matters. Senior candidates know that the JVM can apply lock elision and coarsening to synchronized but not to ReentrantLock.

// COMMON PITFALL

Forgetting unlock() in the non-exceptional path when using ReentrantLock. If the lock is acquired and the try block completes normally but unlock() is only in a catch block, the lock leaks on normal exit. Always put unlock() in finally.