Loading learning content...
Imagine a barbershop with one barber, one barber chair, and a waiting room with N chairs. The barber's life is simple:
Customers' lives are equally simple:
This deceptively simple scenario, formulated by Edsger Dijkstra, captures a fundamental pattern in computing: bounded capacity service with sleeping servers. The Sleeping Barber Problem models connection pooling, thread pools, web servers, ticket counters, and any system where service providers idle when no work exists but must be awakened by arriving requests.
By the end of this page, you will understand the sleeping barber problem comprehensively: its formal specification, race conditions in naive approaches, correct semaphore and monitor solutions, variations (multiple barbers, FIFO ordering, priority), and mapping to real-world systems like connection pools, thread pools, and request handlers.
The sleeping barber problem involves:
Barber (Server): A single service provider who:
Customers (Clients): Processes that arrive seeking service:
Waiting Room (Bounded Buffer): N chairs for waiting customers
Mutual Exclusion: Only one customer in the barber chair at a time.
No Lost Wakeups: If a customer arrives while the barber sleeps, the barber must wake up.
Bounded Waiting Room: At most N customers wait; excess customers leave.
No Deadlock: Neither barber nor customers block indefinitely.
FIFO Fairness (optional but desirable): Customers served in arrival order.
| State | Barber Activity | Waiting Room | Arriving Customer Action |
|---|---|---|---|
| Idle | Sleeping | Empty (0) | Wake barber, get haircut |
| Busy, Room Available | Cutting hair | 1 to N-1 customers | Join waiting room |
| Busy, Room Full | Cutting hair | N customers | Leave immediately (balking) |
The sleeping barber is closely related to producer-consumer with a twist: there's asymmetry between producers (customers) and the consumer (barber). Unlike classical producer-consumer where both sides are symmetric threads, the barber is a distinguished server that sleeps when idle and must be explicitly awakened.
The sleeping barber has a subtle race condition that makes naive implementations incorrect. Let's examine the problem:
Consider this sequence without proper synchronization:
The problem is a lost wakeup: the customer's wakeup signal is lost because the barber wasn't yet sleeping when it arrived.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// BROKEN: Lost wakeup race condition#define N 5 // Waiting room chairsint waiting = 0; // Customers in waiting roombool barber_sleeping = false; void barber(void) { while (true) { if (waiting == 0) { barber_sleeping = true; // RACE: Gap before actually sleeping! sleep(); // May miss wakeup sent in the gap barber_sleeping = false; } // Cut hair waiting--; cut_hair(); }} void customer(void) { if (waiting == N) { leave(); // Waiting room full return; } waiting++; if (barber_sleeping) { wakeup_barber(); // May execute BEFORE barber actually sleeps! } wait_for_haircut(); get_haircut();} /* * RACE CONDITION SCENARIO: * * Time T0: Barber sees waiting == 0 * Time T1: Customer arrives, increments waiting to 1 * Time T2: Customer sees barber_sleeping == false (not yet!) * Time T3: Customer doesn't call wakeup (thinks barber is awake) * Time T4: Barber sets barber_sleeping = true and sleeps * * Result: Barber sleeps forever with 1 customer waiting! */The race occurs because checking the condition and acting on it are not atomic. Both the barber (check waiting → sleep) and customer (check sleeping → wakeup) have a gap between checking and acting.
This is the same lost wakeup problem that plagues any sleep/wakeup mechanism. The solution requires:
Semaphores provide exactly this guarantee: wait() atomically decrements and blocks if zero; signal() atomically increments and wakes waiters.
The check-then-act pattern without synchronization is the root cause of countless concurrency bugs. Any time you see 'if (condition) then action' where both condition and action involve shared state, you must ensure atomicity—either through locks, semaphores, or atomic operations.
The correct solution uses three semaphores:
customers (init = 0): Counting semaphore. Represents waiting customers. Barber waits on this when idle.
barber (init = 0): Binary semaphore. Signals that barber is ready to cut. Customers wait on this for service.
mutex (init = 1): Binary semaphore. Protects the waiting counter from race conditions.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
#include <semaphore.h> #define N 5 // Waiting room chairs sem_t customers; // Number of customers waiting (init = 0)sem_t barber; // Barber ready to cut (init = 0)sem_t mutex; // Protects 'waiting' counter (init = 1)int waiting = 0; // Customers in waiting room void barber_thread(void) { while (true) { // Wait for a customer (blocks if none) sem_wait(&customers); // Decrement waiting count (protected) sem_wait(&mutex); waiting--; sem_post(&mutex); // Signal that barber is ready for this customer sem_post(&barber); // Cut hair (outside critical section) cut_hair(); }} void customer_thread(void) { sem_wait(&mutex); if (waiting < N) { // Room available: join waiting room waiting++; // Signal barber that a customer is waiting sem_post(&customers); sem_post(&mutex); // Wait for barber to be ready sem_wait(&barber); // Get haircut (barber is ready) get_haircut(); } else { // Waiting room full: leave sem_post(&mutex); leave_shop(); }}No Lost Wakeups:
sem_wait(&customers) atomically blocks if no customerssem_post(&customers) atomically wakes barber if blockedMutual Exclusion:
mutex ensures only one thread modifies waiting at a timeBounded Waiting Room:
waiting < N while holding mutexCorrect Handoff:
customersbarber for actual servicebarber after committing to serve| Event | customers | barber | waiting | Description |
|---|---|---|---|---|
| Initial state | 0 | 0 | 0 | Barber blocked on customers |
| Customer 1 arrives | 1→0 | 0 | 1→0 | Barber wakes, customer waits on barber |
| Barber starts cutting | 0 | 1→0 | 0 | Customer unblocked, getting haircut |
| Customer 2, 3 arrive | 2 | 0 | 2 | Barber busy, both wait on barber |
| Barber finishes C1 | 2→1 | 1→0 | 2→1 | C2 gets its turn |
This solution doesn't guarantee FIFO service. When multiple customers wait on the barber semaphore, the wake order depends on the semaphore implementation (often unspecified). For strict FIFO, use a queue with condition variables as shown in the monitor solution.
For guaranteed FIFO service order, we use a monitor with an explicit queue. This is the typical implementation in high-level languages.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
import java.util.LinkedList;import java.util.Queue;import java.util.concurrent.locks.*; public class BarberShop { private final int waitingRoomCapacity; private final Queue<CustomerCallback> waitingCustomers; private boolean barberSleeping; private final Lock lock = new ReentrantLock(); private final Condition customerArrived = lock.newCondition(); public BarberShop(int capacity) { this.waitingRoomCapacity = capacity; this.waitingCustomers = new LinkedList<>(); this.barberSleeping = true; // Barber starts asleep } // Called by barber thread public void barberWork() throws InterruptedException { while (true) { CustomerCallback customer = waitForCustomer(); // Cut hair (outside lock - allows customers to queue) cutHair(); // Signal this specific customer that haircut is done customer.haircutComplete(); } } private CustomerCallback waitForCustomer() throws InterruptedException { lock.lock(); try { while (waitingCustomers.isEmpty()) { barberSleeping = true; customerArrived.await(); // Sleep until signaled } barberSleeping = false; return waitingCustomers.poll(); // FIFO: first in queue } finally { lock.unlock(); } } // Called by customer threads public boolean getHaircut(CustomerCallback callback) throws InterruptedException { lock.lock(); try { if (waitingCustomers.size() >= waitingRoomCapacity) { // Waiting room full return false; // Customer leaves } // Add to queue (FIFO order maintained) waitingCustomers.offer(callback); // Wake barber if sleeping if (barberSleeping) { customerArrived.signal(); } } finally { lock.unlock(); } // Wait for haircut completion (outside lock) callback.awaitHaircut(); return true; } private void cutHair() { // Simulate haircut duration try { Thread.sleep(100); } catch (InterruptedException e) {} }} // Callback for customer to await individual completionclass CustomerCallback { private final Lock lock = new ReentrantLock(); private final Condition done = lock.newCondition(); private boolean complete = false; public void awaitHaircut() throws InterruptedException { lock.lock(); try { while (!complete) { done.await(); } } finally { lock.unlock(); } } public void haircutComplete() { lock.lock(); try { complete = true; done.signal(); } finally { lock.unlock(); } }}Explicit Queue: Using LinkedList<CustomerCallback> ensures exact FIFO order, unlike semaphores where wakeup order is implementation-defined.
Individual Callbacks: Each customer has its own callback for haircut completion. This allows the barber to signal the specific customer being served, not just wake any waiter.
Lock Discipline: The main lock protects the queue and barber state. Actual haircut happens outside the lock to maximize concurrency.
Rejection Semantics: When full, getHaircut() returns false immediately. The customer can retry or give up—application decides.
Java's ThreadPoolExecutor implements the sleeping barber pattern internally. Worker threads sleep when the work queue is empty, wake when tasks arrive, and the queue is bounded (causing rejection when full). The implementation uses ReentrantLock and Condition, exactly as shown here.
Real barbershops (and real servers) often have multiple barbers. Extending the solution to M barbers requires careful consideration.
The simplest extension: multiple barbers share a single customer queue. This naturally load-balances—whichever barber finishes first takes the next customer.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
#include <semaphore.h> #define M 3 // Number of barbers#define N 10 // Waiting room capacity sem_t customers; // Customers waiting (init = 0)sem_t barber_ready; // Available barbers (init = M)sem_t mutex; // Protects waiting counter int waiting = 0; // Each barber runs thisvoid barber_thread(int barber_id) { while (true) { // Wait for a customer sem_wait(&customers); // Get exclusive access to waiting count sem_wait(&mutex); waiting--; sem_post(&mutex); // Signal that this barber is handling a customer sem_post(&barber_ready); printf("Barber %d cutting hair", barber_id); cut_hair(); }} void customer_thread(int customer_id) { sem_wait(&mutex); if (waiting < N) { waiting++; sem_post(&customers); // Signal barbers sem_post(&mutex); sem_wait(&barber_ready); // Wait for any available barber printf("Customer %d getting haircut", customer_id); get_haircut(); } else { sem_post(&mutex); printf("Customer %d leaving - shop full", customer_id); }}Alternatively, each barber can have their own queue. Customers choose a barber (perhaps the one with the shortest queue). This provides:
Trade-off: Potential load imbalance if customers guess wrong about queue lengths.
Advanced implementations allow idle barbers to "steal" customers from busy queues:
This combines the benefits of dedicated queues (locality) with automatic load balancing.
| Strategy | Load Balance | Latency Predictability | Implementation |
|---|---|---|---|
| Single Shared Queue | Excellent | Variable (FIFO) | Simple |
| Dedicated Queues | Customer-dependent | Predictable | Moderate |
| Work-Stealing | Good | Mostly predictable | Complex |
Java's ForkJoinPool uses work-stealing. Each worker thread has a deque (double-ended queue). Workers push/pop from their own end (LIFO for cache locality) but steal from others' ends (FIFO for fairness). This balances locality with load distribution.
Real-world services often have priority customers (first class, premium members, urgent requests). Extending sleeping barber for priorities introduces interesting challenges.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
import java.util.PriorityQueue;import java.util.Comparator;import java.util.concurrent.locks.*; public class PriorityBarberShop { private final int capacity; private final PriorityQueue<PriorityCustomer> queue; private boolean barberSleeping = true; private final Lock lock = new ReentrantLock(); private final Condition customerArrived = lock.newCondition(); public PriorityBarberShop(int capacity) { this.capacity = capacity; // Higher priority number = more important = served first this.queue = new PriorityQueue<>( Comparator.comparingInt(PriorityCustomer::getPriority).reversed() .thenComparing(PriorityCustomer::getArrivalTime) ); } public boolean enterShop(int customerId, int priority) throws InterruptedException { lock.lock(); try { if (queue.size() >= capacity) { return false; // Full, even VIP must leave } PriorityCustomer customer = new PriorityCustomer( customerId, priority, System.nanoTime() ); queue.offer(customer); if (barberSleeping) { customerArrived.signal(); } } finally { lock.unlock(); } // Await service... return true; } public void barberServe() throws InterruptedException { while (true) { PriorityCustomer next; lock.lock(); try { while (queue.isEmpty()) { barberSleeping = true; customerArrived.await(); } barberSleeping = false; next = queue.poll(); // Highest priority first } finally { lock.unlock(); } System.out.printf("Serving customer %d (priority %d)%n", next.getId(), next.getPriority()); cutHair(); } }} class PriorityCustomer { private final int id; private final int priority; private final long arrivalTime; // Constructor and getters...}Priority systems risk starving low-priority customers under high load:
| Time | Arrivals | Queue State | Served |
|---|---|---|---|
| T0 | P1 (priority=1) | [P1] | |
| T1 | V1 (priority=5) | [V1, P1] | |
| T2 | [P1] | V1 | |
| T3 | V2 (priority=5) | [V2, P1] | |
| T4 | [P1] | V2 | |
| T5 | V3 (priority=5) | [V3, P1] | |
| ... | VIPs keep coming | [Vn, P1] | P1 starves! |
Aging: Gradually increase priority of waiting customers. Eventually, a waiting regular customer's boosted priority exceeds VIPs.
Quota per Priority: "Serve at most 3 VIPs before one regular customer"
Maximum Wait Time: If a customer waits beyond threshold, promote to highest priority.
Weighted Fair Queuing: Each priority level gets proportional share of service (e.g., VIPs get 70%, regular 30%).
In systems where customers can hold resources while waiting, priority schemes can cause priority inversion: a high-priority customer waits for a resource held by a low-priority customer who can't proceed because medium-priority customers keep preempting them. This is exactly the problem that crashed Mars Pathfinder!
The sleeping barber pattern is ubiquitous in systems software. Every bounded-capacity server with idle waiting exhibits this pattern.
12345678910111213141516171819202122232425262728293031323334353637
import java.util.concurrent.*; public class ThreadPoolAsBarberShop { public static void main(String[] args) { // Create a "barbershop" with: // - 3 barbers (core pool size) // - Max 5 barbers if overwhelmed (max pool size) // - 10-seat waiting room (queue capacity) BlockingQueue<Runnable> waitingRoom = new ArrayBlockingQueue<>(10); // Bounded capacity! ThreadPoolExecutor shop = new ThreadPoolExecutor( 3, // Core barbers 5, // Max barbers 60, TimeUnit.SECONDS, // Idle barber goes home after 60s waitingRoom, new ThreadPoolExecutor.AbortPolicy() // Reject when full ); // Customers (tasks) arrive for (int i = 0; i < 20; i++) { final int customerId = i; try { shop.execute(() -> { System.out.println("Cutting hair: " + customerId); try { Thread.sleep(200); } catch (InterruptedException e) {} }); System.out.println("Customer " + customerId + " admitted"); } catch (RejectedExecutionException e) { System.out.println("Customer " + customerId + " rejected - shop full!"); } } shop.shutdown(); }}Once you recognize the sleeping barber pattern—servers that sleep when idle, bounded queues for waiting, wakeup on arrival, rejection when full—you'll see it everywhere. Thread pools, connection pools, web servers, and message queues are all variations of this fundamental abstraction.
Implementation of sleeping barber systems often encounters these issues:
Thread Dumps: When workers appear stuck, a thread dump (jstack in Java, gdb in C) shows what each thread is waiting on.
Queue Metrics: Monitor queue size over time. Constantly near capacity indicates under-provisioned workers or slow processing.
Latency Histograms: Track time from queue entry to service start. Bimodal distribution suggests priority inversion or lock contention.
Rejection Logging: Log every rejection with timestamp and queue state. Cluster analysis reveals traffic patterns causing rejections.
1234567891011121314151617181920
// Poison pill pattern for graceful shutdownpublic class ShutdownableBarberShop { private final BlockingQueue<Task> queue; private static final Task POISON_PILL = new Task(null); public void shutdown() { queue.offer(POISON_PILL); } void barberLoop() throws InterruptedException { while (true) { Task task = queue.take(); if (task == POISON_PILL) { queue.offer(POISON_PILL); // Pass poison to next barber break; } process(task); } }}The sleeping barber problem captures the essence of bounded-capacity services with sleeping workers. Let's consolidate the key insights:
What's Next:
We've now covered four classic OS synchronization problems: producer-consumer, readers-writers, dining philosophers, and sleeping barber. In the final page of this module, we'll step back and examine Problem-Solving Strategies—systematic approaches for analyzing new synchronization problems and designing correct solutions from first principles.
You now possess comprehensive understanding of the sleeping barber problem: the wakeup race condition, correct semaphore and monitor solutions, variations for multiple workers and priorities, and how the pattern manifests in thread pools, connection pools, and servers throughout systems software. This knowledge enables you to design and debug any bounded-capacity service system.