The Core Architecture
1. Basic Locking & Mutual Exclusion
- The Goal: Prevent a race condition where multiple threads attempt to modify a shared resource simultaneously.
- The Mechanism: Only one thread can successfully invoke
.lock()and enter the critical section at a time. All other threads are put into a waiting queue until the lock owner calls.unlock(). - The Safety Rule: You must place
.unlock()inside afinallyblock. If an exception occurs inside your business logic without it, the lock remains held forever, deadlocking the system.
2. The Meaning of “Re-entrant”
- Self-Blocking Avoidance: A single thread that already owns the lock can safely re-enter another code block protected by the exact same lock without deadlocking itself.
- The Hold Count: The lock maintains a counter (
getHoldCount()). Every nested call to.lock()by the owner increments this count. Every call to.unlock()decrements it. The resource is only fully released when the count returns to zero.
3. Execution Control: Fair vs. Unfair
- Unfair (Default): Maximizes performance by allowing new incoming threads to “barge in”. If a thread requests a lock exactly when it is released, it bypasses the queue and claims it immediately, boosting throughput.
- Fair (
new ReentrantLock(true)): Enforces a strict first-come, first-served sequence. Threads wait in a FIFO queue. While it prevents thread starvation, managing this strict queue reduces execution speed.
4. Advanced Lock Interrogation (tryLock)
- Non-Blocking Strategy:
.tryLock()attempts to claim the lock and instantly returnstrueif successful, orfalseif held by someone else—allowing your thread to pivot to alternate tasks instead of freezing. - Timeouts:
.tryLock(timeout, timeUnit)instructs the thread to poll for the lock for a specified duration before giving up.

Concrete Example – Movie Seat Booking
Let’s apply this directly to the multi-threaded movie theater booking example.
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
public class MovieTheater {
private final ReentrantLock lock = new ReentrantLock(); // Defaults to Unfair for speed
private final Set<String> bookedSeats = new HashSet<>();
public void bookSeat(String customerName, String seatNumber) {
System.out.println(customerName + " is attempting to book seat " + seatNumber);
lock.lock(); // Thread safely acquires the lock block
try {
// Critical Section: Only one thread can check and modify this block at a time
if (!bookedSeats.contains(seatNumber)) {
System.out.println("Processing payment for " + customerName + "...");
Thread.sleep(100); // Simulate network latency
bookedSeats.add(seatNumber);
System.out.println("SUCCESS: " + seatNumber + " booked for " + customerName);
} else {
System.out.println("FAILED: " + seatNumber + " is already taken.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock(); // Always release the lock so waiting threads can proceed
}
}
}
Advanced Signaling with newCondition()
Mutual exclusion solves the problem of writing data safely, but it doesn’t solve coordination (waiting for state changes). That is where lock.newCondition() comes in.
1. Conceptual Breakdown
- The Root Problem: Traditional synchronization only gives you a single wait-set (
wait()andnotify()). If you notify, you wake up a random thread, which might be waiting for an entirely different reason. - The Solution: A
Conditionobject creates a distinct, isolated waiting line for specific conditions while reusing the same lock framework. You can have multiple independent condition lines attached to one lock.
2. The Restaurant Waiting Buzzer
Imagine a busy restaurant to understand these concepts:
- The Lock: The restaurant door functions as the lock. Only one group at a time can talk to the host at the podium to check for a table.
- The Condition (
.await()): A group asks for a large table, but none are currently ready. Rather than standing there and blocking the podium—which would create a deadlock—the host hands the group a waiting buzzer, similar to callingCondition.await(), and the group waits elsewhere. Receiving the buzzer releases the podium (the lock) so other customers can approach the host. - The Signal (
.signal()): When a large table finally clears, the host presses a button to light up the waiting group’s buzzer, similar to callingCondition.signal(). The group is alerted, returns to the podium, re-acquires the lock, and gets seated.

3. How it Fits Into the ReentrantLock
A Condition is strictly bound to a specific lock instance.
- Creation: Conditions are created directly from the lock using
lock.newCondition(). - Requirement: A thread must currently hold the lock to use a
Condition. condition.await(): When a thread callsawait(), it instantly releases the lock and places itself into a waiting queue specific to that condition. The thread then stays dormant until it receives a signal.condition.signal(): Callingsignal()wakes up exactly one thread waiting on that specific condition. The awakened thread must then wait to re-acquire the lock before it can resume execution from where it paused.condition.signalAll(): This wakes up all threads currently waiting on that specific condition.
4. Coding the Analogy: A Managed Dining Lounge
Here is how you implement multiple conditions (tableAvailable and podiumFree) on a single lock to manage a restaurant waiting layout smoothly:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class RestaurantLounge {
private final ReentrantLock hostLock = new ReentrantLock();
// Separate queues for separate issues
private final Condition smallTableAvailable = hostLock.newCondition();
private final Condition largeTableAvailable = hostLock.newCondition();
private int freeSmallTables = 0;
private int freeLargeTables = 0;
public void waitForTable(String partyName, int partySize) throws InterruptedException {
hostLock.lock();
try {
if (partySize <= 2) {
while (freeSmallTables == 0) {
System.out.println(partyName + " (Party of " + partySize + ") is handed a Small-Table Buzzer.");
smallTableAvailable.await(); // Releases hostLock, sits down to wait
}
freeSmallTables--;
System.out.println(partyName + " has been seated at a small table.");
} else {
while (freeLargeTables == 0) {
System.out.println(partyName + " (Party of " + partySize + ") is handed a Large-Table Buzzer.");
largeTableAvailable.await(); // Releases hostLock, sits down to wait
}
freeLargeTables--;
System.out.println(partyName + " has been seated at a large table.");
}
} finally {
hostLock.unlock();
}
}
public void tableBecomesFree(int tableSize) {
hostLock.lock();
try {
if (tableSize <= 2) {
freeSmallTables++;
System.out.println("--- A small table cleared up. Buzzing a 2-person party ---");
smallTableAvailable.signal(); // Targets only the small table waiting line
} else {
freeLargeTables++;
System.out.println("--- A large table cleared up. Buzzing a family party ---");
largeTableAvailable.signal(); // Targets only the large table waiting line
}
} finally {
hostLock.unlock();
}
}
}

Key Matrix Dry Run
| Step | Active Thread | Lock Owner | Small Tables | Small Table Wait Queue | Large Table Wait Queue |
|---|---|---|---|---|---|
| Start | None | None 🔓 | 0 | Empty | Empty |
| 1 | Alice | Alice 🔒 | 0 | Empty | Empty |
| 2 | Alice (await) | None 🔓 | 0 | [Alice 😴] | Empty |
| 3 | Bob (await) | None 🔓 | 0 | [Alice 😴] | [Bob 😴] |
| 4 | Staff(signal) | Staff 🔒 | 1 | [Alice 😴] | [Bob 😴] |
| 5 | Staff exits | None 🔓 | 1 | Empty (Alice waking) | [Bob 😴] |
| 6 | Alice (Seated) | Alice 🔒 | 0 | Empty | [Bob 😴] |
| End | None | None 🔓 | 0 | Empty | [Bob 😴] |
Leave a Reply