Home/Learn/Java A–Z/ReentrantLock and Locks

ReentrantLock and Locks

Advanced
Concurrency

java.util.concurrent.locks provides explicit locking with more flexibility than synchronized — tryLock, timed locking, multiple conditions, and fairness.

Overview

ReentrantLock from java.util.concurrent.locks provides all the capabilities of synchronized plus more: tryLock() to avoid deadlocks, timed lock acquisition, multiple Condition objects for fine-grained wait/notify, and optional fairness. ReadWriteLock allows concurrent reads while serializing writes. StampedLock (Java 8) adds optimistic reading. Explicit locks must always be unlocked in a finally block.

ReentrantLock Basics

ReentrantLock is reentrant — the same thread can acquire the lock multiple times without deadlocking (just as synchronized is reentrant). It must be unlocked the same number of times it was locked.

Always call unlock() in a finally block to guarantee release even if an exception is thrown.

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

public class SafeCounter {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock();

    public void increment() {
        lock.lock();          // acquire
        try {
            count++;
        } finally {
            lock.unlock();    // ALWAYS release in finally
        }
    }

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

    // tryLock — non-blocking attempt
    public boolean tryIncrement() {
        if (lock.tryLock()) {
            try {
                count++;
                return true;
            } finally {
                lock.unlock();
            }
        }
        return false; // lock not available
    }
}

Condition Variables

ReentrantLock supports multiple Condition objects — each is like a separate wait-set. This is more powerful than the single wait-set of synchronized (Object.wait/notify).

Use case: a BoundedBuffer with separate "not full" and "not empty" conditions. notFull.signal() only wakes producers; notEmpty.signal() only wakes consumers — more efficient than notifyAll().

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

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

    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == capacity)
                notFull.await();         // wait on "not full"
            queue.add(item);
            notEmpty.signal();           // wake one consumer
        } finally { lock.unlock(); }
    }

    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty())
                notEmpty.await();        // wait on "not empty"
            T item = queue.poll();
            notFull.signal();            // wake one producer
            return item;
        } finally { lock.unlock(); }
    }
}

ReadWriteLock and StampedLock

ReadWriteLock allows multiple concurrent readers but exclusive writers. This is ideal for read-heavy caches.

StampedLock (Java 8) adds optimistic reading: read without locking, then validate the stamp — if valid, no lock was needed; if not, upgrade to a full read lock. Much faster for read-heavy workloads.

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

// ReadWriteLock — concurrent reads, exclusive write
public class Cache<K, V> {
    private final Map<K, V>   map  = new HashMap<>();
    private final ReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock          r   = rwl.readLock();
    private final Lock          w   = rwl.writeLock();

    public V get(K key) {
        r.lock();                     // shared read lock
        try { return map.get(key); }
        finally { r.unlock(); }
    }

    public void put(K key, V value) {
        w.lock();                     // exclusive write lock
        try { map.put(key, value); }
        finally { w.unlock(); }
    }
}

// StampedLock — optimistic read
StampedLock sl = new StampedLock();
double x, y;

long stamp = sl.tryOptimisticRead();  // no lock acquired
x = this.x; y = this.y;              // read values
if (!sl.validate(stamp)) {           // check if write occurred
    stamp = sl.readLock();           // upgrade to read lock
    try { x = this.x; y = this.y; }
    finally { sl.unlockRead(stamp); }
}

Interactive Visualization

NEWRUNNABLERUNNINGBLOCKEDWAITINGTERMINATED
synchronized(lock)— free
main
RUNNING
t1
NEW
t2
NEW
main thread creates Thread t1 and Thread t2. Both are in NEW state.
1 / 6

Key Points to Remember

  • Always unlock in a finally block — otherwise a lock is permanently held on exception.
  • tryLock() avoids deadlocks by allowing a thread to back off if the lock is unavailable.
  • Multiple Condition objects allow fine-grained wait/signal (unlike synchronized's single wait-set).
  • ReadWriteLock: multiple concurrent readers, exclusive writer — great for read-heavy caches.
  • StampedLock optimistic reading avoids locking when no concurrent write occurred.

Practice ReentrantLock and Locks in the Playground

Run and modify code directly in your browser - no setup needed.

Interview Questions

Sign in to ask Aria
1

What advantages does ReentrantLock have over synchronized?

MediumAmazon
2

Why must unlock() be called in a finally block?

EasyGoogle
3

How does ReadWriteLock improve throughput for read-heavy workloads?

MediumOracle
4

What is the difference between Condition.await() and Object.wait()?

MediumMicrosoft
5

What is optimistic locking in StampedLock?

HardNetflix

Ask Aria about ReentrantLock and Locks

Your personal AI tutor — ask anything about this concept