🚀AdvancedAdvanced Concurrency

ReentrantLock and Conditions

Use ReentrantLock and Condition for fine-grained thread synchronization beyond synchronized blocks.

Overview

ReentrantLock is a more flexible alternative to the synchronized keyword. Like synchronized, it ensures mutual exclusion, but it also supports features that synchronized cannot offer: timed lock attempts, interruptible lock waits, and multiple Condition objects per lock. A Condition object acts like a wait/notify pair bound to a specific lock. Multiple conditions per lock let you wake only threads waiting for a specific event, rather than waking all waiting threads (which is what notifyAll() does with synchronized). Reentrant means the same thread can acquire the lock multiple times without deadlocking itself. The lock has a hold count that must be matched with an equal number of unlock() calls.

Key Points

  • •lock.lock() acquires the lock; lock.unlock() releases it
  • •Always call unlock() in a finally block to prevent lock leaks
  • •lock.tryLock() returns true if the lock is available, false otherwise (no blocking)
  • •lock.tryLock(timeout, unit) attempts to acquire within a time window
  • •lock.lockInterruptibly() allows the thread to be interrupted while waiting
  • •lock.newCondition() creates a Condition bound to this lock
  • •condition.await() releases the lock and waits (like Object.wait)
  • •condition.signal() wakes one thread waiting on this condition
  • •condition.signalAll() wakes all threads waiting on this condition
  • •ReadWriteLock provides separate read and write locks for better read concurrency

Examples

Basic ReentrantLock replacing synchronized

import java.util.concurrent.locks.ReentrantLock;

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

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

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

    public static void main(String[] args) throws InterruptedException {
        Counter c = new Counter();
        Thread t1 = new Thread(() -> { for (int i = 0; i < 10000; i++) c.increment(); });
        Thread t2 = new Thread(() -> { for (int i = 0; i < 10000; i++) c.increment(); });
        t1.start(); t2.start();
        t1.join(); t2.join();
        System.out.println(c.get()); // always 20000
    }
}

The finally block guarantees unlock even if an exception occurs inside the critical section. Forgetting finally is a common source of deadlocks.

Condition variables for producer-consumer coordination

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

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

    BoundedQueue(int maxSize) { this.maxSize = maxSize; }

    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == maxSize) notFull.await();
            queue.add(item);
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) notEmpty.await();
            T item = queue.poll();
            notFull.signal();
            return item;
        } finally {
            lock.unlock();
        }
    }
}

Two separate conditions (notFull, notEmpty) mean put only wakes threads blocked on take, and vice versa. With a single Object.wait/notifyAll, all threads would be woken even when the wake-up does not apply to them.

tryLock for non-blocking attempts

import java.util.concurrent.locks.ReentrantLock;

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

    public static void main(String[] args) {
        Runnable task = () -> {
            if (lock.tryLock()) {
                try {
                    System.out.println(Thread.currentThread().getName() + " acquired lock");
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    lock.unlock();
                }
            } else {
                System.out.println(Thread.currentThread().getName() + " could not acquire lock, skipping");
            }
        };

        new Thread(task, "t1").start();
        new Thread(task, "t2").start();
    }
}

tryLock() returns immediately. One thread will log "acquired" and the other will log "could not acquire". Use tryLock(timeout, unit) to wait up to a time limit before giving up.

Common Mistakes

  • Not calling unlock() in a finally block. If the critical section throws, the lock stays acquired forever, deadlocking any other thread that tries to acquire it.
  • Using lock.notify() or lock.wait(). These methods belong to Object and cannot be used with ReentrantLock. Use Condition.signal() and Condition.await() instead.
  • Checking condition with if instead of while before await(). Spurious wakeups can occur; always recheck the condition in a while loop.
  • Acquiring the lock in one method and forgetting to release it in another code path. Because tryLock can return false, callers sometimes forget the early-return path skips unlock.

Note

Use ReentrantReadWriteLock when reads are frequent and writes are rare. Multiple threads can hold the read lock simultaneously, but a write lock is exclusive. This can dramatically improve throughput for read-heavy data structures.

Exercises

Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.

Loading editor…

Completed: 0 / 2 exercises

Knowledge Check

Question 1 of 4

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