DevNotes
Support
Multithreading · Chapter 08

Explicit Locks — ReentrantLock, ReadWriteLock & StampedLock

The Lock interface, lock/unlock in try-finally, tryLock and lockInterruptibly, fairness, Condition variables, ReentrantReadWriteLock, StampedLock optimistic reads, and synchronized vs ReentrantLock.

◷ 8 min read§ 11 sections? 7 interview questions{ } 7 code examples

#Introduction

synchronized is simple but inflexible: you can't time out while waiting, can't respond to interrupts while blocked, can't check whether a lock is free, and have only one wait set per lock. The java.util.concurrent.locks package (Java 5) adds explicit locks that solve all of these.

#The Lock Interface

MethodBehaviour
lock()Acquire, waiting as long as necessary (not interruptible)
lockInterruptibly()Acquire, but abort with InterruptedException if interrupted while waiting
tryLock()Acquire only if free right now; returns true/false immediately
tryLock(time, unit)Wait up to a timeout; interruptible
unlock()Release
newCondition()Create a Condition (a separate wait set) bound to this lock

#ReentrantLock

Java
class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count;

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

Like intrinsic locks, ReentrantLock is reentrant — the owning thread can lock it again, and must unlock it the same number of times. getHoldCount(), isLocked() and isHeldByCurrentThread() help with debugging and assertions.

tryLock — Don't Wait Forever

Java
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
    try {
        updateInventory();
    } finally {
        lock.unlock();
    }
} else {
    throw new ServiceBusyException("Inventory is busy, try again");
}

Useful for deadlock avoidance (see the previous chapter) and for failing fast under contention instead of piling up blocked request threads.

lockInterruptibly — Cancellable Waiting

Java
lock.lockInterruptibly();   // a thread blocked here can be cancelled with interrupt()
try {
    process();
} finally {
    lock.unlock();
}

A thread blocked on synchronized or lock() ignores interrupts until it gets the lock; lockInterruptibly() lets executors cancel it (shutdownNow(), future.cancel(true)).

Fairness

Java
ReentrantLock fairLock = new ReentrantLock(true);
Unfair (default)Fair
A newly arriving thread may "barge" ahead of waiting threadsLongest-waiting thread gets the lock next
Much higher throughputLower throughput
Possible starvationPrevents starvation

Use fairness only when starvation is a real problem — the throughput cost is significant. (Note: tryLock() without a timeout ignores fairness.)

#Condition Variables

A Condition is like wait/notify, but a lock can have several of them — so producers and consumers can wait in separate queues:

Java
class BoundedBuffer<T> {
    private final Queue<T> items = new ArrayDeque<>();
    private final int capacity;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull  = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

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

    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            while (items.size() == capacity) {
                notFull.await();          // producers wait here
            }
            items.add(item);
            notEmpty.signal();            // wake exactly one consumer
        } finally {
            lock.unlock();
        }
    }

    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (items.isEmpty()) {
                notEmpty.await();         // consumers wait here
            }
            T item = items.remove();
            notFull.signal();             // wake exactly one producer
            return item;
        } finally {
            lock.unlock();
        }
    }
}
Object monitorCondition
wait()await()
notify()signal()
notifyAll()signalAll()
One wait set per objectMany conditions per lock

The same rules apply: hold the lock, and await() in a while loop. This is essentially how ArrayBlockingQueue is implemented.

#ReentrantReadWriteLock

Many workloads are read-heavy — a cache read thousands of times per second and refreshed occasionally. An exclusive lock makes readers wait for each other needlessly. A read-write lock allows many concurrent readers or one writer:

Java
class ProductCache {
    private final Map<Long, Product> cache = new HashMap<>();
    private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    private final Lock readLock  = rw.readLock();
    private final Lock writeLock = rw.writeLock();

    public Product get(long id) {
        readLock.lock();              // many threads can hold this at once
        try {
            return cache.get(id);
        } finally {
            readLock.unlock();
        }
    }

    public void put(long id, Product p) {
        writeLock.lock();             // exclusive: waits for all readers to leave
        try {
            cache.put(id, p);
        } finally {
            writeLock.unlock();
        }
    }
}
HeldAnother read lock?Another write lock?
Read lock(s)✅ Allowed❌ Blocks
Write lock❌ Blocks❌ Blocks

#StampedLock

StampedLock (Java 8) adds optimistic reads — a read that takes no lock at all, then validates that no write happened:

Java
class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();

    double distanceFromOrigin() {
        long stamp = sl.tryOptimisticRead();     // no locking
        double cx = x, cy = y;
        if (!sl.validate(stamp)) {               // a write happened — fall back
            stamp = sl.readLock();
            try {
                cx = x;
                cy = y;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return Math.hypot(cx, cy);
    }

    void move(double dx, double dy) {
        long stamp = sl.writeLock();
        try {
            x += dx;
            y += dy;
        } finally {
            sl.unlockWrite(stamp);
        }
    }
}

Very fast for read-mostly data, but StampedLock is not reentrant, has no Condition support, and is easy to misuse. Use it only for measured hot spots.

#synchronized vs ReentrantLock

FeaturesynchronizedReentrantLock
SyntaxKeyword; block structuredAPI calls; must unlock() in finally
Automatic release✅ On block exit or exception❌ Manual
Reentrant✅✅
Try without blocking / timeout❌✅ tryLock(), tryLock(timeout)
Interruptible while waiting❌✅ lockInterruptibly()
Fairness option❌✅
Multiple wait sets❌ One per monitor✅ Many Conditions
Lock across methods❌ Must be in one block✅ Lock in one method, unlock in another (use carefully)
InspectionThread.holdsLock() onlyisLocked(), getQueueLength(), …
PerformanceComparable on modern JVMsComparable

#Interview Questions

Q1What is the difference between synchronized and ReentrantLock?

Both are reentrant mutual-exclusion locks with the same memory semantics. ReentrantLock adds tryLock (with timeout), interruptible locking, optional fairness and multiple Conditions, but must be unlocked manually in finally. synchronized is simpler and releases automatically.

Q2Why must unlock() be in a finally block?

Because an explicit lock isn't released on exceptions. Without finally, an exception leaves the lock held forever and blocks every other thread.

Q3What is a fair lock?

A lock that grants access in roughly first-come-first-served order to the longest-waiting thread, preventing starvation at the cost of throughput.

Q4What is a ReadWriteLock and when do you use it?

A pair of locks allowing many concurrent readers or one exclusive writer. Use it for read-heavy data with infrequent writes and non-trivial read sections.

Q5Can you upgrade a read lock to a write lock in ReentrantReadWriteLock?

No — it deadlocks. Release the read lock first, then acquire the write lock (and re-check state). Downgrading from write to read is supported.

Q6What is a Condition?

A wait set associated with a Lock, providing await(), signal() and signalAll(). A lock can have several conditions, allowing different kinds of waiters to be signalled separately.

Q7What is StampedLock's optimistic read?

A read that takes no lock: it gets a stamp, reads the fields, then calls validate(stamp). If a write occurred, it falls back to a real read lock.

#Common Mistakes

  • Forgetting unlock(), or not putting it in finally.
  • Calling lock() inside the try — if lock() fails, finally calls unlock() on a lock you don't hold.
  • Trying to upgrade a read lock to a write lock.
  • Using fair locks by default — large throughput loss for no benefit.
  • Calling await()/signal() without holding the lock → IllegalMonitorStateException.
  • Using StampedLock reentrantly — it isn't reentrant and will deadlock.

#Best Practices

  • Use synchronized unless you need a feature only Lock provides.
  • Always follow the lock(); try { … } finally { unlock(); } idiom.
  • Use tryLock(timeout) on request paths where blocking indefinitely is unacceptable.
  • Use separate Conditions for different wait reasons.
  • Measure before introducing read-write or stamped locks; consider concurrent collections first.

#Key Takeaways

  • ReentrantLock adds timeouts, interruptibility, fairness and multiple conditions to what synchronized offers.
  • Explicit locks must be released in finally.
  • Condition gives each wait reason its own queue — precise signalling instead of notifyAll().
  • ReentrantReadWriteLock lets readers share; writers are exclusive; upgrading deadlocks.
  • StampedLock offers lock-free optimistic reads for read-mostly hot spots.