Concurrency

57. Locks and Conditions

ReentrantLock offers more flexibility than synchronized โ€” timed and interruptible lock attempts, fairness, and Condition variables; ReadWriteLock allows concurrent reads; StampedLock adds optimistic reading.

Java 21 (core language)

Explanation

ReentrantLock must always be unlocked in a finally block to guarantee release even if an exception occurs. Unlike synchronized, it supports tryLock() (non-blocking attempt), tryLock(time, unit) (timed attempt), lockInterruptibly() (responds to interrupt), and a fairness option (new ReentrantLock(true) โ€” threads acquire in FIFO order, preventing starvation but reducing throughput).

ReentrantReadWriteLock gives separate read and write locks. Multiple threads can hold the read lock simultaneously; the write lock is exclusive. Good for data structures read far more often than written.

StampedLock (Java 8+) adds optimistic reading: try to read without acquiring any lock (tryOptimisticRead() returns a stamp), read data, then validate the stamp. If validation fails (a write occurred), fall back to a pessimistic read lock. This gives maximum read throughput when writes are rare.

Example

import java.util.concurrent.locks.*;

public class LocksAndConditions {
    // Bounded buffer using ReentrantLock + Conditions
    static class BoundedBuffer<T> {
        private final Object[] items;
        private int head, tail, count;
        private final ReentrantLock lock = new ReentrantLock();
        private final Condition notFull  = lock.newCondition();
        private final Condition notEmpty = lock.newCondition();

        BoundedBuffer(int capacity) { items = new Object[capacity]; }

        @SuppressWarnings("unchecked")
        public T take() throws InterruptedException {
            lock.lock();
            try {
                while (count == 0) notEmpty.await();
                T item = (T) items[head];
                items[head] = null;
                head = (head + 1) % items.length;
                count--;
                notFull.signal();
                return item;
            } finally { lock.unlock(); }
        }

        public void put(T item) throws InterruptedException {
            lock.lock();
            try {
                while (count == items.length) notFull.await();
                items[tail] = item;
                tail = (tail + 1) % items.length;
                count++;
                notEmpty.signal();
            } finally { lock.unlock(); }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        BoundedBuffer<Integer> buf = new BoundedBuffer<>(3);

        Thread prod = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try { buf.put(i); System.out.println("Put: " + i); }
                catch (InterruptedException e) { Thread.currentThread().interrupt(); }
            }
        });

        Thread cons = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try { System.out.println("Got: " + buf.take()); }
                catch (InterruptedException e) { Thread.currentThread().interrupt(); }
            }
        });

        prod.start(); cons.start();
        prod.join();  cons.join();
    }
}

Sources