# Java synchronized vs ReentrantLock: 7 Critical Differences Explained

> Explore critical differences between Java synchronized and ReentrantLock. Understand JVM versus explicit locks, interruptible acquisition, fairness, and conditions for better concurrency control.

- Repository: [CyC2018/CS-Notes](https://github.com/CyC2018/CS-Notes)
- Tags: deep-dive
- Published: 2026-02-24

---

**While both `synchronized` and `ReentrantLock` provide mutual exclusion for Java threads, `synchronized` is a JVM-level implicit lock using `monitorenter` and `monitorexit` bytecode instructions, whereas `ReentrantLock` is an explicit Java class in `java.util.concurrent.locks` that offers interruptible acquisition, fairness policies, and multiple `Condition` objects.**

When building thread-safe concurrent applications in Java, developers must choose between the built-in `synchronized` keyword and the `ReentrantLock` class. According to the comprehensive concurrency notes in the CyC2018/CS-Notes repository—specifically detailed in `notes/Java 并发.md` in the "比较" (comparison) section around line 491—these two synchronization mechanisms differ fundamentally in implementation, flexibility, and control granularity. Understanding these distinctions is essential for writing high-performance, maintainable multithreaded code.

## Implementation Mechanism: JVM Bytecode vs. Java Class

The most fundamental difference lies in how each mechanism is implemented at the runtime level.

**`synchronized`** is a native JVM feature managed through object monitors. When you use the `synchronized` keyword, the Java compiler generates `monitorenter` and `monitorexit` bytecode instructions. The JVM handles the monitor lock directly through object headers and native synchronization primitives, as documented in `notes/Java 并发.md` around line 337.

**`ReentrantLock`** is a pure Java class (`java.util.concurrent.locks.ReentrantLock`) that implements the `Lock` interface. It maintains lock state internally using atomic variables and manages waiting threads through an Abstract Queued Synchronizer (AQS). While it delegates to native code for low-level atomic operations, the locking logic and queue management are implemented in Java, providing greater flexibility at the cost of explicit API usage.

## Lock Acquisition: Implicit vs. Explicit

The syntax and responsibility for lock management differ significantly between the two approaches.

With **`synchronized`**, lock acquisition is **implicit**. You declare synchronization through:

- Synchronized blocks: `synchronized (obj) { ... }`
- Synchronized methods: `synchronized void method()`

The JVM automatically acquires the monitor when entering the block or method and guarantees release when exiting, even if an exception is thrown.

With **`ReentrantLock`**, acquisition is **explicit**. You must manually call `lock.lock()` to acquire the lock and `lock.unlock()` to release it. This requires careful exception handling, typically using a `try…finally` block to ensure the lock is released regardless of exceptions:

```java
lock.lock();
try {
    // critical section
} finally {
    lock.unlock();  // guaranteed release
}

```

Failure to call `unlock()` in a `finally` block can lead to permanent deadlocks if an exception occurs in the critical section.

## Interruptibility: Responding to Thread Cancellation

One of the most significant functional differences is how each mechanism handles thread interruption during lock acquisition.

**`synchronized`** is **not interruptible**. Once a thread is blocked waiting to enter a synchronized block or method, it cannot be woken up by calling `Thread.interrupt()`. The thread will remain blocked until the lock becomes available, completely ignoring interruption requests during the wait.

**`ReentrantLock`** supports **interruptible lock acquisition** through the `lockInterruptibly()` method. As detailed in `notes/Java 并发.md` around line 505, this method allows a waiting thread to respond to interruption by throwing `InterruptedException`, enabling responsive cancellation policies:

```java
try {
    lock.lockInterruptibly();
    try {
        // critical section
    } finally {
        lock.unlock();
    }
} catch (InterruptedException e) {
    // handle cancellation gracefully
    Thread.currentThread().interrupt();
}

```

This capability is essential for implementing responsive systems where long-running operations must be cancellable.

## Fairness Policies: Preventing Thread Starvation

Fairness determines whether the lock favors the thread that has been waiting the longest.

**`synchronized`** uses a **non-fair** policy by default. The JVM makes no guarantees about which waiting thread acquires the monitor next when the lock becomes available. While the JVM implementation may favor the longest-waiting thread, there is no strict fairness contract, and thread starvation—where some threads never acquire the lock—is possible under high contention.

**`ReentrantLock`** provides configurable **fairness**. You can construct a fair lock using `new ReentrantLock(true)`, which guarantees that the longest-waiting thread acquires the lock next. This prevents starvation but may reduce overall throughput due to the overhead of maintaining the ordered queue and waking threads in sequence.

## Condition Support: Single vs. Multiple Wait Sets

The mechanisms differ in how threads wait for specific conditions within the lock.

**`synchronized`** provides a **single implicit condition** per monitor through the `Object` class methods: `wait()`, `notify()`, and `notifyAll()`. All threads waiting for different conditions must share the same wait set, which can lead to inefficiency because `notifyAll()` wakes all waiting threads even if only some can proceed.

**`ReentrantLock`** supports **multiple distinct conditions** through the `Condition` interface. By calling `lock.newCondition()`, you can create separate condition objects, each maintaining its own wait set. This allows fine-grained control where you can signal threads waiting for specific conditions without waking threads waiting for different conditions, improving efficiency in complex coordination scenarios.

## Performance Characteristics

Modern JVMs have optimized both mechanisms, but trade-offs remain.

**`synchronized`** benefits from extensive JVM optimizations including **biased locking**, **lock elision**, and **lightweight locks**. In uncontended or low-contention scenarios, `synchronized` often outperforms `ReentrantLock` because it avoids the overhead of Java object allocation and method dispatch, operating directly on the object monitor in native code.

**`ReentrantLock`** incurs slightly higher baseline overhead due to additional method calls and the allocation of `Node` objects for the internal AQS queue. However, this overhead is negligible in high-contention scenarios or when utilizing advanced features like fairness or interruptibility that `synchronized` cannot provide.

## Practical Code Examples

The following examples demonstrate the syntactic and semantic differences between these locking mechanisms.

### Basic synchronized Usage

This example shows implicit lock acquisition through synchronized methods, where the JVM handles `monitorenter` and `monitorexit` automatically:

```java
public class SyncCounter {
    private int count = 0;

    public synchronized void increment() {
        count++;               // monitorenter / monitorexit handled by JVM
    }

    public synchronized int get() {
        return count;
    }
}

```

### Explicit Locking with ReentrantLock

This example demonstrates explicit lock management using `lock()` and `unlock()` with proper `try…finally` exception handling:

```java
import java.util.concurrent.locks.*;

public class LockCounter {
    private final Lock lock = new ReentrantLock();   // non-fair lock
    private int count = 0;

    public void increment() {
        lock.lock();                                 // explicit acquisition
        try {
            count++;
        } finally {
            lock.unlock();                           // guaranteed release
        }
    }

    public int get() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

```

### Advanced ReentrantLock Features

This bounded buffer example showcases interruptible lock acquisition (`lockInterruptibly()`) and multiple `Condition` objects for fine-grained producer-consumer coordination:

```java
import java.util.concurrent.locks.*;

public class BoundedBuffer<T> {
    private final Lock lock = new ReentrantLock();
    private final Condition notFull  = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    private final Object[] items = new Object[10];
    private int putPtr, takePtr, count;

    public void put(T x) throws InterruptedException {
        lock.lockInterruptibly();               // can be interrupted while waiting
        try {
            while (count == items.length)
                notFull.await();               // await on specific condition
            items[putPtr] = x;
            if (++putPtr == items.length) putPtr = 0;
            ++count;
            notEmpty.signal();                 // wake a consumer
        } finally {
            lock.unlock();
        }
    }

    @SuppressWarnings("unchecked")
    public T take() throws InterruptedException {
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            Object x = items[takePtr];
            if (++takePtr == items.length) takePtr = 0;
            --count;
            notFull.signal();                  // wake a producer
            return (T) x;
        } finally {
            lock.unlock();
        }
    }
}

```

## When to Use synchronized vs. ReentrantLock

Choosing the right synchronization mechanism depends on your specific concurrency requirements and complexity needs.

**Prefer `synchronized` when:**
- You need simple mutual exclusion without advanced features like timeout or interruptibility
- The locking logic is straightforward and contained within a single method or block
- You want automatic lock release to prevent deadlocks from forgotten `unlock()` calls
- You are working in low-contention scenarios where JVM optimizations (biased locking, lock elision) provide excellent performance

**Choose `ReentrantLock` when:**
- You need to interrupt a thread waiting for a lock using `lockInterruptibly()`
- You require timed lock attempts via `tryLock(long timeout, TimeUnit unit)`
- You need fairness guarantees to prevent thread starvation (`new ReentrantLock(true)`)
- You require multiple wait-sets/conditions for complex thread coordination patterns
- You need to query lock statistics or implement sophisticated locking policies

## Summary

- **`synchronized`** is a JVM-native implicit lock using `monitorenter` and `monitorexit` bytecode, offering automatic scope-based release but limited flexibility.
- **`ReentrantLock`** is an explicit Java class providing interruptible acquisition (`lockInterruptibly()`), optional fairness, timed lock attempts (`tryLock()`), and multiple `Condition` objects.
- **Interruptibility** is a key differentiator: `synchronized` blocks cannot respond to `Thread.interrupt()`, while `ReentrantLock` supports interruption during lock acquisition.
- **Fairness** is only configurable with `ReentrantLock` via the constructor parameter; `synchronized` always uses a non-fair policy.
- **Performance** is comparable in modern JVMs, but `synchronized` benefits from biased locking and lock elision optimizations, while `ReentrantLock` incurs slight overhead for its additional features.

## Frequently Asked Questions

### Can I use ReentrantLock as a drop-in replacement for synchronized?

While `ReentrantLock` can replace `synchronized` for mutual exclusion, it is not a direct drop-in replacement due to its explicit API. You must manually call `unlock()` in a `finally` block to prevent deadlocks, whereas `synchronized` handles release automatically. Additionally, `ReentrantLock` cannot be used with the `synchronized` keyword syntax, requiring refactoring of lock acquisition logic throughout your codebase.

### Is ReentrantLock faster than synchronized?

In modern JVMs, `synchronized` is often faster for uncontended or low-contention scenarios due to optimizations like biased locking, lightweight locks, and lock elision. `ReentrantLock` incurs slightly higher overhead from additional method calls and object allocations for its internal queue nodes. However, in high-contention scenarios or when using advanced features like fairness, `ReentrantLock` provides better control and potentially better throughput through its sophisticated queuing mechanisms.

### How does lockInterruptibly() work differently from lock()?

The `lockInterruptibly()` method allows a thread waiting to acquire a lock to respond to interruption by throwing `InterruptedException`, whereas `lock()` ignores interrupts during the waiting period. When using `lock()`, if a thread is interrupted while waiting for the lock, it continues waiting until the lock is acquired, then sets the interrupt status flag upon return. In contrast, `lockInterruptibly()` immediately throws `InterruptedException` if the thread is interrupted while waiting, allowing for responsive cancellation without acquiring the lock.

### Can synchronized be made fair like ReentrantLock?

No, the `synchronized` keyword does not support configurable fairness policies. The JVM implementation typically uses a non-fair ordering where the next thread to acquire the monitor is not guaranteed to be the one that has been waiting the longest. While specific JVM implementations may favor the longest-waiting thread, there is no strict fairness contract, and thread starvation is possible under high contention. If strict fairness is required, you must use `ReentrantLock` constructed with `new ReentrantLock(true)`.