Back to Locks: ReentrantLock, ReadWriteLock, StampedLock
Q1Question 1 of 7

What is ReentrantLock vs synchronized?

Key Concepts
  • ReentrantLock provides tryLock() (non-blocking), tryLock(timeout) (deadline), lockInterruptibly(), fairness, and multiple Conditions.
  • synchronized is simpler and safer: auto-releases on exception via two monitorexit instructions in bytecode.
  • ReentrantLock MUST be unlocked in a finally block; forgetting causes a permanent lock hold.
  • Fair mode (new ReentrantLock(true)) ensures FIFO ordering but is about 10% slower.
  • Condition objects (lock.newCondition()) provide multiple wait-sets per lock, unlike synchronized which has only one.
  • Use synchronized for simple mutual exclusion; use ReentrantLock when you need tryLock, timeout, interruptibility, or multiple Conditions.

Definition

ReentrantLock offers tryLock, timeout, interruptibility, fairness, and multiple Conditions that synchronized cannot provide, but requires manual unlock in a finally block.
  • Lock interface – java.util.concurrent.locks.Lock defines lock(), unlock(), tryLock(), tryLock(timeout, unit), lockInterruptibly(), and newCondition().
  • ReentrantLock – the main implementation of Lock. It is reentrant: the same thread can acquire the lock multiple times (must unlock the same number of times).
  • tryLock() – attempts to acquire the lock without blocking. Returns false immediately if unavailable. Prevents deadlock.
  • tryLock(timeout) – waits up to the specified timeout. Returns false if the lock is not acquired in time.
  • lockInterruptibly() – can be interrupted while waiting (unlike synchronized, which cannot).
  • Fairness – new ReentrantLock(true) ensures the longest-waiting thread gets the lock next (FIFO). Prevents starvation.
  • Condition – lock.newCondition() creates a separate wait-set. Multiple Conditions per lock enable targeted wake-ups (e.g., notEmpty for consumers, notFull for producers).

synchronized is an automatic sliding door: opens when you approach, closes when you leave. Simple, reliable, you cannot forget to close it. But it has no timeout, no cancel, no "is it open?" check, and only one waiting line. ReentrantLock is a manual padlock with features: tryLock checks if it is open without waiting, timeout waits for 5 minutes maximum, interruptible lets the manager cancel your wait, fair mode serves the longest-waiting person first, and multiple Conditions create separate waiting rooms.

Understanding the Concepts

How do you use ReentrantLock?

The critical pattern: always unlock in a finally block.

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.concurrent.locks.ReentrantLock;

public class AccountWithLock {
    private final ReentrantLock lock = new ReentrantLock();
    private BigDecimal balance;

    public AccountWithLock(BigDecimal initialBalance) {
        this.balance = initialBalance;
    }

    public void debit(BigDecimal amount) {
        lock.lock();
        try {
            if (balance.compareTo(amount) < 0) {
                throw new IllegalStateException("Insufficient funds");
            }
            balance = balance.subtract(amount);
        } finally {
            lock.unlock(); // MUST be in finally!
        }
    }
}

How does tryLock prevent deadlock?

JAVA
package com.bank.service;

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

public class TryLockDemo {
    private final ReentrantLock lock = new ReentrantLock();

    // Non-blocking: returns immediately
    public boolean tryProcess() {
        if (lock.tryLock()) {
            try {
                System.out.println("Processing trade");
                return true;
            } finally { lock.unlock(); }
        }
        return false; // lock not available
    }

    // With timeout: waits up to 5 seconds
    public boolean tryProcessWithTimeout() throws InterruptedException {
        if (lock.tryLock(5, TimeUnit.SECONDS)) {
            try {
                System.out.println("Processing trade");
                return true;
            } finally { lock.unlock(); }
        }
        System.out.println("Could not acquire lock in 5 seconds");
        return false;
    }
}

How do Conditions work?

JAVA
package com.bank.service;

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class BoundedTradeQueue {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notEmpty = lock.newCondition();
    private final Condition notFull = lock.newCondition();
    private final Queue<String> queue = new LinkedList<>();
    private final int capacity;

    public BoundedTradeQueue(int capacity) { this.capacity = capacity; }

    public void put(String trade) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() >= capacity) notFull.await();
            queue.add(trade);
            notEmpty.signal(); // wake ONE consumer
        } finally { lock.unlock(); }
    }

    public String take() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) notEmpty.await();
            String trade = queue.poll();
            notFull.signal(); // wake ONE producer
            return trade;
        } finally { lock.unlock(); }
    }
}

How do ReentrantLock and synchronized compare?

| Feature | synchronized | ReentrantLock |
|---|---|---|
| Auto-release on exception | Yes (two monitorexit) | No (must use finally) |
| tryLock (non-blocking) | No | Yes |
| Timeout | No | Yes (tryLock with timeout) |
| Interruptible wait | No | Yes (lockInterruptibly) |
| Fairness | No | Yes (fair=true) |
| Multiple Conditions | No (one wait-set) | Yes (newCondition) |
| Syntax | Simple (block-structured) | Verbose (lock/try/finally/unlock) |
| Reentrant | Yes | Yes |

What is reentrant behavior?

A reentrant lock allows the same thread to acquire it multiple times. Each acquisition increments a hold count. The lock is released only when the hold count reaches zero.

JAVA
package com.bank.core;

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantDemo {
    private final ReentrantLock lock = new ReentrantLock();

    public void outer() {
        lock.lock(); // hold count = 1
        try {
            inner(); // same thread: hold count = 2
        } finally { lock.unlock(); } // hold count = 1
    }

    public void inner() {
        lock.lock(); // hold count = 2 (same thread, no deadlock)
        try {
            System.out.println("Inside inner");
        } finally { lock.unlock(); } // hold count = 1
    }
}

When should you use which?

  • Use synchronized for simple mutual exclusion where you do not need tryLock, timeout, or multiple Conditions.
  • Use ReentrantLock when you need: non-blocking tryLock, timed tryLock for deadlock prevention, interruptible lock waits, fair ordering, or multiple Conditions for producer-consumer patterns.

What should a senior engineer know?

  • AQS (AbstractQueuedSynchronizer): ReentrantLock is built on AQS, which uses a CLH queue for thread parking and CAS for state management.
  • StampedLock (Java 8): provides optimistic read locking. The reader gets a stamp, reads the data, and validates the stamp. If no writer intervened, the read succeeds without any lock acquisition. Much faster than ReadWriteLock for read-heavy workloads.
  • Lock poisoning: if a thread encounters an unrecoverable error inside a lock, it can set a "poisoned" flag. Other threads check the flag after acquiring the lock and throw immediately.

Visual Overview

DIAGRAM
  synchronized vs ReentrantLock:

  synchronized:                    ReentrantLock:
  ┌─────────────────────┐          ┌──────────────────────────┐
  │ synchronized (lock) {│          │ lock.lock();              │
  │   // critical section│          │ try {                     │
  │ }                    │          │   // critical section     │
  │ // auto-released     │          │ } finally {               │
  │ // even on exception │          │   lock.unlock(); // MUST! │
  └─────────────────────┘          │ }                         │
                                   └──────────────────────────┘

  ReentrantLock extras:
  ┌────────────────────────────┐
  │ tryLock()     → non-block  │
  │ tryLock(5, s) → timeout    │
  │ lockInterruptibly() → IE   │
  │ new ReentrantLock(true)    │
  │               → fair queue │
  │ lock.newCondition()        │
  │               → multi-wait │
  └────────────────────────────┘

Key Takeaway

ReentrantLock provides features that synchronized cannot: tryLock() for non-blocking attempts, tryLock(timeout) for deadline-based acquisition, lockInterruptibly() for cancellable waits, fair mode for FIFO ordering, and newCondition() for multiple wait-sets. However, it requires manual unlock() in a finally block. Forgetting causes a permanent lock hold.

synchronized is simpler and safer for basic mutual exclusion: the JVM guarantees lock release on both normal and exceptional exits. Use synchronized as the default. Switch to ReentrantLock only when you need its specific features.

Remember one sentence: use synchronized for simple locking and ReentrantLock when you need tryLock, timeout, interruptibility, fairness, or multiple Conditions.

---

Share
Use ← → arrow keys to navigate