skip to content

What does the fairness flag on a Semaphore control, and what is the tradeoff?

level: seniorimportance: should knowfreq 45%

answer

  1. Fairness = which waiter gets the next released permit
  2. Fair = FIFO, no starvation, lower throughput
  3. Non-fair (default) = barging allowed, higher throughput, can starve
  4. Barging avoids a wake-up/handoff context switch
  5. Untimed tryAcquire() barges even on a fair semaphore

basics

~20 s

The fairness flag decides the order waiting threads get permits. Fair (true) serves them first-come-first-served (FIFO), preventing any thread from waiting forever. Non-fair (the default) lets a new thread grab a free permit ahead of those already waiting, which is faster but can starve waiters.

solid answer

~50 s

new Semaphore(n, fair) takes a boolean. With fair=true, permits are granted in FIFO order of the AQS wait queue — a thread can't be overtaken, so no thread starves. With fair=false (default), the semaphore allows barging: a thread arriving exactly when a permit is released can grab it before threads that have been queued longer. Barging avoids the context-switch of waking a queued thread and handing off, so non-fair has higher throughput under contention; that's why it's the default. The cost is potential starvation — an unlucky thread may wait far longer than others, hurting tail latency and predictability. Note a wrinkle: the untimed tryAcquire() barges regardless of the fairness setting, so it can still jump a fair queue. Choose fair when bounded, predictable wait time matters (latency SLAs, fairness across clients); choose non-fair for maximum throughput when occasional long waits are acceptable.

go deeper

for a junior

Knows there is a fairness flag and that fair means first-come-first-served.

for a middle

Explains barging in non-fair mode and that fairness prevents starvation at a throughput cost.

for a senior

Articulates the throughput-vs-starvation tradeoff, why non-fair is the default, and the tryAcquire barging exception; picks fairness from latency/SLA requirements.

for a principal

Reasons about fairness within the AQS framework and at system scale — tail-latency, tenant fairness, and benchmarking the throughput cost under realistic contention.

## What "fairness" means here When several threads are **blocked** in `acquire()` waiting for a permit, and a permit becomes available (someone calls `release()`), *which* waiting thread gets it? That ordering policy is what the **fairness flag** controls. ```java new Semaphore(5); // non-fair (default) new Semaphore(5, true); // fair ``` ## Fair (true): FIFO, no overtaking Waiting threads sit in a first-in-first-out queue (the `AbstractQueuedSynchronizer` wait queue that `Semaphore` is built on). A fair semaphore grants the next permit to the **longest-waiting** thread. Critically, a brand-new thread calling `acquire()` will **not** snatch a permit if others are already queued — it goes to the back of the line. This guarantees **no starvation**: every waiter is eventually served in order, so wait times are bounded and predictable. ## Non-fair (false, the default): barging allowed A non-fair semaphore permits **barging**. When a permit is released, a thread that happens to call `acquire()` at that very moment can grab the permit **immediately**, even though other threads have been waiting longer. The newcomer "jumps the queue." Why is this the default? **Throughput.** Handing a permit to a queued thread requires waking it up — a context switch and scheduling delay. If a running thread can take the permit right now without that handoff, the CPU does useful work sooner. Under heavy contention, barging measurably increases throughput because it avoids a stream of wake-up/handoff round-trips. ## The tradeoff in one line - **Fair** → predictable, no starvation, lower throughput. - **Non-fair** → higher throughput, but a thread can **starve** (wait indefinitely / suffer terrible tail latency) if it keeps getting barged past. **Starvation** = a thread is perpetually denied progress because others keep being chosen ahead of it. Fairness is the standard cure. ## A subtle exception: tryAcquire() ignores fairness The untimed, no-argument `tryAcquire()` **always barges**, *even on a fair semaphore*. It checks for a free permit and takes it without consulting the wait queue. So if you rely on fairness for ordering, be aware that `tryAcquire()` callers can still cut the line. The **timed** `tryAcquire(timeout, unit)`, by contrast, queues and honours fairness. ## How to choose | Situation | Pick | |---|---| | Latency SLA / bounded wait per request | fair | | Fairness across many clients/tenants | fair | | Maximise throughput, occasional long waits OK | non-fair (default) | | Low/no contention (rarely any waiters) | non-fair — fairness barely matters | When contention is low, the choice is almost irrelevant (there's rarely a queue to overtake). The decision matters under **sustained contention**, where barging's throughput win and starvation risk both become real. ## Cost of fairness Fairness isn't free at the mechanism level either: a fair acquire must check the queue and often park even when a permit is technically available, adding overhead. Benchmarks generally show non-fair outperforming fair under contention — which is precisely why `Semaphore`, `ReentrantLock`, etc. all default to non-fair.

  • Why is non-fair the default for Semaphore and ReentrantLock?
    Throughput. Barging lets a running thread take a freed permit without the wake-up and context switch needed to hand off to a parked, queued thread. Under contention this avoids many handoff round-trips, so non-fair has higher throughput — at the cost of possible starvation, which most workloads tolerate.
  • Does setting fairness=true guarantee no thread ever waits long?
    It guarantees FIFO ordering so no thread is overtaken by latecomers (no starvation). But the absolute wait still depends on how long permit-holders run; fairness bounds your position in line, not the wall-clock time, and the untimed tryAcquire() can still barge past you.

saying these in an interview costs you the question

  • Thinking fairness controls performance only, not ordering/starvation
  • Assuming fair semaphores are always better — they cost throughput
  • Believing fairness prevents tryAcquire() from barging (the untimed form still does)
  • Worrying about fairness when there's essentially no contention

context