Show how to use a Semaphore to bound concurrent access to a resource pool, and explain the correctness pitfalls.
answer
- Permits == capacity; acquire before borrow, release in finally
- Acquire: permit then poll; Release: return item then permit
- Leak = release missing -> capacity shrinks to deadlock
- Over-release after failed tryAcquire breaks the N bound
- Semaphore counts, doesn't track which object/thread
basics
~20 sCreate 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.
solid answer
~50 sThe canonical pattern: size the Semaphore to the resource's capacity (e.g. N pooled connections), acquire() before borrowing, and release() in a finally so the permit returns even on exception. This bounds concurrent borrowers to N — any extra threads block until a permit frees. The classic pitfalls are: (1) releasing outside finally, so an exception leaks a permit and the pool silently shrinks toward deadlock; (2) releasing without having acquired (e.g. in a finally after a failed tryAcquire), which over-counts permits and lets more than N threads in, defeating the bound; (3) treating the semaphore as if it tracks identity — it only counts, so you still need real ownership logic for which connection a thread holds. The semaphore guards capacity, not the objects themselves; pair it with a thread-safe data structure (e.g. a BlockingQueue or ConcurrentLinkedQueue) holding the actual pooled items.
go deeper
Can write the acquire/use/release-in-finally pattern and explain that permit count equals capacity.
Implements a correct pool, orders item-return vs permit-release correctly, and avoids the leak and over-release bugs.
Compares Semaphore-based pools with BlockingQueue, reasons about the count-vs-identity distinction, and picks fairness based on starvation/latency needs.
Evaluates pooling strategy at system level — admission control, back-pressure, fairness, and observability of leaks/over-release — and decides build-vs-use-a-library.
## Goal: cap concurrent users of a limited resource Suppose you have a pool of **5 expensive resources** (database connections, file handles, GPU slots). You want at most 5 threads using them at once; the 6th and beyond should wait. A `Semaphore` sized to 5 is exactly the gate. ## A minimal correct pool ```java public final class BoundedPool<T> { private final Semaphore permits; private final Queue<T> items; // thread-safe holder of the real objects public BoundedPool(Collection<T> resources) { this.permits = new Semaphore(resources.size(), true); // fair: FIFO this.items = new ConcurrentLinkedQueue<>(resources); } public T acquire() throws InterruptedException { permits.acquire(); // 1. gate on capacity (blocks past N) return items.poll(); // 2. take an actual item (never null here) } public void release(T item) { if (item == null) return; items.offer(item); // 3. return the object FIRST permits.release(); // 4. THEN release the permit } } ``` ### Why the ordering matters On **release**, return the object to the queue *before* releasing the permit. Otherwise a just-woken thread could `permits.acquire()` succeed, then `items.poll()` and find the queue momentarily empty. On **acquire**, gate the permit *before* polling, so a successful `acquire()` guarantees an item is present (invariant: free-permit-count == items-in-queue). ## The standard call site ```java T r = pool.acquire(); try { use(r); } finally { pool.release(r); // ALWAYS, even on exception } ``` ## Pitfall 1 — release outside `finally` (permit leak) ```java sem.acquire(); use(r); // if this throws... sem.release(); // ...this never runs -> permit leaked forever ``` Each leak permanently lowers capacity. After 5 leaks on a `Semaphore(5)`, *every* `acquire()` blocks forever — a slow-motion **deadlock** that's hard to diagnose because nothing crashes; threads just pile up. Fix: `release()` in `finally`. ## Pitfall 2 — release without acquire (over-release) ```java if (sem.tryAcquire()) { /* ... */ } finally { sem.release(); } // BUG: releases even when tryAcquire returned false ``` Because a semaphore has no owner and `release()` blindly increments the count, this **inflates** permits above the intended N. Now 6, 7, … threads slip past the gate and the capacity bound is silently broken — you might exhaust the underlying resource and get errors that look unrelated. Fix: only release on the path that actually acquired. ```java boolean got = sem.tryAcquire(); if (got) { try { /* ... */ } finally { sem.release(); } } ``` ## Pitfall 3 — confusing counting with ownership A `Semaphore` only **counts**; it does not know *which* object a thread holds or even *which* thread holds a permit. So you must keep the real pooled objects in a separate thread-safe structure and never assume the semaphore tracks identity. It guards the *number* of concurrent holders, full stop. ## Why a semaphore and not just a lock or a blocking queue? - A `ReentrantLock` allows only one holder — wrong shape for N. - A `BlockingQueue` of N items *is* a valid alternative pool (take/put block on capacity) and is often preferred because the item and the permit are unified. The semaphore version shines when the permit and the resource are conceptually separate, or when you want timed/fail-fast admission (`tryAcquire`) and a tunable fairness policy independent of the data structure. ## Fairness for pools A **fair** semaphore (`new Semaphore(n, true)`) serves waiters FIFO, preventing a thread from **starving** (waiting indefinitely while latecomers barge). For a pool under steady contention, fairness gives predictable latency at a small throughput cost; non-fair maximises throughput but can starve unlucky threads.
- Why might a BlockingQueue be a simpler pool than a Semaphore plus a queue?A BlockingQueue unifies the permit and the item: take() blocks when empty (capacity gate) and returns the actual object atomically; put() returns it. There's no separate count to keep in sync, eliminating the acquire/poll ordering and over-release bugs. Use a Semaphore when you need separate admission control (timed/fail-fast/fairness) decoupled from the data structure.
- What symptom does a leaked permit produce?No crash — just gradually rising blocked-thread counts as effective capacity drops, ending in a hang where every acquire() waits forever. It looks like a deadlock or thread-pool exhaustion and is hard to trace back to a missing release().
saying these in an interview costs you the question
- release() outside finally, leaking permits on exceptions
- Releasing in a finally even when tryAcquire returned false
- Assuming the semaphore tracks which resource a thread holds
- Releasing the permit before returning the object to the pool