skip to content

Semaphore

A Semaphore hands out a bounded number of permits, which is how you cap concurrent access to a scarce resource rather than enforce mutual exclusion. It is also the idiomatic way to limit concurrency in a virtual-thread world, which makes it a current interview favorite.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

5

What is a Semaphore in Java and what problem does it solve?

level: juniorimportance: must knowfreq 70%

answer

  1. Bowl of permits: acquire takes, release returns
  2. Counting = N at once; binary(1) ≈ non-reentrant mutex
  3. No owner, not reentrant, can release more than acquired
  4. release() in finally — leaked permit deadlocks everyone
  5. tryAcquire to fail fast; fairness flag = FIFO vs barging

basics

~20 s

A Semaphore holds a set number of permits. A thread calls acquire() to take a permit (waiting if none are free) and release() to give it back. It limits how many threads can use a resource at once.

solid answer

~40 s

java.util.concurrent.Semaphore is a counting synchronizer that maintains a number of permits. acquire() takes one permit, blocking if none are available; release() returns one, potentially unblocking a waiter. It bounds concurrent access to a shared resource — for example, capping simultaneous connections to a pool at N by creating a Semaphore with N permits. Unlike a lock, a semaphore has no notion of an owner: any thread may release, and you can release more permits than you acquired (which grows the pool). A Semaphore(1) acts like a non-reentrant mutex. tryAcquire() lets you take a permit without blocking (or with a timeout), and the fairness flag controls whether waiting threads are served FIFO.

go deeper

for a junior

Can describe permits, acquire(), release(), and that a semaphore limits how many threads use a resource at once. Knows release goes in finally.

for a middle

Distinguishes counting vs binary semaphores, explains tryAcquire for non-blocking attempts, and knows a semaphore has no owner and isn't reentrant.

for a senior

Contrasts Semaphore with ReentrantLock (owner, reentrancy), reasons about permit leaks and release-without-acquire as deliberate signalling, and chooses fair vs non-fair by starvation/throughput tradeoff.

for a principal

Frames Semaphore within the AQS-based synchronizer family, weighs it against pool libraries and rate limiters, and reasons about fairness/starvation and back-pressure design at the system level.

## The problem Imagine a shared resource that can only safely handle a limited number of users at the same time — say a connection pool with 5 connections, or a downstream service that allows at most 10 concurrent calls. If 100 threads rush in, you need a gatekeeper that lets only N through and makes the rest wait. A **mutex** (mutual-exclusion lock) only allows *one* at a time. A **Semaphore** generalises this to *N* at a time. ## Permits A `Semaphore` is created with a number of **permits**: `new Semaphore(5)` starts with 5. Think of permits as physical tickets in a bowl: - **`acquire()`** removes one ticket from the bowl. If the bowl is empty, the calling thread **blocks** (waits, doing nothing, not spinning) until another thread puts a ticket back. - **`release()`** puts one ticket back into the bowl, and if any thread is waiting in `acquire()`, one of them is woken up and proceeds. The count of available permits goes up and down as threads acquire and release. When it hits zero, the next `acquire()` waits. ## The standard pattern ```java Semaphore sem = new Semaphore(5); sem.acquire(); // take a permit (may block) try { useTheResource(); // at most 5 threads are here at once } finally { sem.release(); // ALWAYS give it back } ``` The `release()` belongs in a `finally` block so the permit is returned even if the work throws an exception. Forgetting this **leaks** a permit — the bowl permanently shrinks, and eventually everyone blocks forever. ## No owner — the key difference from a lock A `ReentrantLock` or `synchronized` block has an **owner**: only the thread that locked it may unlock it, and it is **reentrant** (the owner can re-lock without deadlocking). A `Semaphore` has **neither**: - Any thread may call `release()`, even one that never called `acquire()`. This is intentional — it lets you use a semaphore as a signal between producer and consumer threads. - It is **not reentrant**: if the same thread calls `acquire()` twice on a `Semaphore(1)`, it deadlocks against itself. - You can `release()` more than you acquired, which *increases* the permit count above the starting value — a legitimate way to add capacity to a pool at runtime. Because of this, a `Semaphore` is *not* a drop-in lock for protecting a critical section where you need reentrancy or owner checks. A `Semaphore(1)` is a **binary semaphore** that behaves like a non-reentrant mutex, useful mainly when you need cross-thread signalling. ## Counting vs binary - **Counting semaphore**: permits > 1 — bounds concurrency to N (pool limiting, rate gating). - **Binary semaphore**: 1 permit — mutual exclusion or a single on/off signal. ## Non-blocking and timed acquisition `tryAcquire()` attempts to take a permit and returns `true`/`false` *immediately* without blocking — useful to shed load ("if no capacity, fail fast"). `tryAcquire(timeout, unit)` waits up to a bound. You can also acquire/release **multiple permits at once**: `acquire(3)` / `release(3)`. ## Fairness The constructor takes an optional boolean: `new Semaphore(5, true)` makes it **fair** — waiting threads are granted permits in FIFO (first-come-first-served) order. The default (`false`) is **non-fair**: a newly arriving thread may "barge" and grab a just-released permit ahead of threads that have been waiting longer. Non-fair has higher throughput (fewer context switches); fair prevents **starvation** (a thread waiting indefinitely) at some throughput cost. ## Under the hood `Semaphore` is built on `AbstractQueuedSynchronizer` (AQS), the same framework behind `ReentrantLock` and `CountDownLatch`. The permit count is AQS's shared integer state, and waiting threads sit in AQS's FIFO wait queue. ## When to reach for it Resource pools, throttling concurrent requests to a service, implementing a bounded buffer's capacity, or simple cross-thread signalling. When you just need exclusive access to a critical section, prefer `synchronized` or `ReentrantLock` (reentrant, owner-checked); use a semaphore when the *count* of concurrent holders matters.

  • How is a Semaphore(1) different from a ReentrantLock?
    Both allow one holder, but ReentrantLock is reentrant (same thread can re-lock) and owner-checked (only the locker unlocks). Semaphore(1) is non-reentrant (re-acquire self-deadlocks) and ownerless (any thread can release), so it suits cross-thread signalling, not general critical sections.
  • What happens if you forget to release a permit?
    That permit is gone for good. The available count drops permanently; once enough leak, every acquire() blocks forever and the system deadlocks. Always release in a finally block.

saying these in an interview costs you the question

  • Calling a Semaphore a reentrant lock — it has no owner and is not reentrant
  • Putting release() outside finally so an exception leaks the permit
  • Believing only the acquiring thread may release (any thread can)
  • Thinking permits can never exceed the initial count (release adds capacity)

context

open as a page

Show how to use a Semaphore to bound concurrent access to a resource pool, and explain the correctness pitfalls.

level: middleimportance: must knowfreq 60%

basics

~20 s

Create a Semaphore with as many permits as the resource allows. Each user acquires a permit before using the resource and releases it in a finally block afterward. That caps the number of simultaneous users at the permit count.

open as a page

How does tryAcquire() differ from acquire(), and when would you use the timed or multi-permit variants?

level: middleimportance: should knowfreq 55%

basics

~20 s

acquire() waits until a permit is free. tryAcquire() takes a permit only if one is available right now and returns true; otherwise it returns false immediately without waiting. tryAcquire(timeout) waits up to a limit before giving up.

open as a page

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

level: seniorimportance: should knowfreq 45%

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.

open as a page

When would you choose a Semaphore over a lock, CountDownLatch, or BlockingQueue — and what are its limitations?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Use a Semaphore when you need to limit how many threads use something at once (N at a time). Use a lock for exclusive access by one thread. Use a CountDownLatch to wait until events finish. Use a BlockingQueue to hand data between threads. A Semaphore only counts permits; it doesn't track owners or carry data.

open as a page