What is a Semaphore in Java and what problem does it solve?
answer
- Bowl of permits: acquire takes, release returns
- Counting = N at once; binary(1) ≈ non-reentrant mutex
- No owner, not reentrant, can release more than acquired
- release() in finally — leaked permit deadlocks everyone
- tryAcquire to fail fast; fairness flag = FIFO vs barging
basics
~20 sA 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 sjava.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
Can describe permits, acquire(), release(), and that a semaphore limits how many threads use a resource at once. Knows release goes in finally.
Distinguishes counting vs binary semaphores, explains tryAcquire for non-blocking attempts, and knows a semaphore has no owner and isn't reentrant.
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.
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)