Hook: Interviewers love semaphores because they test whether you can control crowding in a system, not just protect data.
Question: What is a semaphore in Java?
Answer: A semaphore is a counter of permits. A thread calls acquire() to take a permit and release() to give one back; if no permits are available, the thread blocks or can fail fast with tryAcquire(). In Java, Semaphore is in java.util.concurrent and is used to limit how many threads can enter a section at the same time.
Interview-Ready Answer: I use a semaphore when I want to limit concurrency, not just protect a single critical section. It gives me a fixed number of permits, so only that many threads can proceed at once, and the rest wait. In Java, I’d use Semaphore for things like capping access to a database pool or an external API, and I’d always release permits in a finally block to avoid leaks.
Detailed Explanation: Think of a semaphore as a box of tickets. Each ticket is a permit—a simple allowed entry token. If the box has 5 tickets, at most 5 threads can hold one at a time. A binary semaphore has 1 permit and behaves a bit like a lock, while a counting semaphore has many permits and is more flexible.
new Semaphore(3).acquire(). If a permit exists, the count is decremented immediately and the thread continues.release(). The permit count increases, and one waiting thread may be unparked and allowed to continue.new Semaphore(permits, true), Java tries to hand permits out in FIFO order; with the default unfair mode, a newly arriving thread may sometimes grab a permit sooner and get better throughput.One important advanced detail: semaphore operations provide the memory visibility you expect in concurrency. In practice, actions done before release() become visible to a thread after it successfully acquire()s that permit.
| Tool | Main job | Ownership | Reuse? |
|---|---|---|---|
| Semaphore | Limit concurrent access | No | Yes |
| Lock | Mutual exclusion | Yes | Yes |
| CountDownLatch | Wait for events | No | No |
A lock is for “only one thread at a time,” while a semaphore is for “up to N at a time.” A CountDownLatch is for waiting until some work finishes, not for limiting access. Also, a semaphore is not owned by the thread that acquired it, so another thread can release it—useful, but also easy to misuse.
acquire() and release() is O(1) for the permit counter.finally; otherwise a thrown exception can leak a permit and gradually freeze the system.availablePermits() is only a snapshot, not a guarantee for future safety.tryAcquire() with a timeout is useful for fail-fast behavior when waiting is worse than dropping or retrying.Memory mental model: “A semaphore is a parking lot with N spots: if all spots are full, cars wait outside; when a car leaves, one waiting car can enter.”
Real-World Example: Imagine a checkout service in an e-commerce app that calls a payment provider. The provider only handles a small number of concurrent requests well, so each app instance uses a semaphore with 20 permits. That keeps traffic smooth: at most 20 payment calls run at once, and the rest wait briefly instead of overwhelming the gateway.
What goes wrong when people misunderstand it? A developer puts release() outside a finally block. One exception path skips the release, permits slowly leak, and after enough failures every request blocks on acquire(). In production, you see thread pools stuck, rising queue time, and logs like “waiting for payment slot” while users see endless spinners or checkout timeouts. If someone over-releases instead, the app may flood the gateway and trigger 429 rate-limit errors.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class SemaphoreDemo {
public static void main(String[] args) throws InterruptedException {
// Three permits means at most three workers may enter the critical section at once.
Semaphore semaphore = new Semaphore(3);
ExecutorService pool = Executors.newFixedThreadPool(6);
CountDownLatch done = new CountDownLatch(6);
AtomicInteger active = new AtomicInteger(0);
AtomicInteger maxActive = new AtomicInteger(0);
List<Runnable> tasks = new ArrayList<>();
for (int i = 1; i <= 6; i++) {
final int taskId = i;
tasks.add(() -> {
boolean acquired = false;
try {
semaphore.acquire();
acquired = true;
// This counter proves the semaphore is actually capping concurrency.
int nowActive = active.incrementAndGet();
maxActive.accumulateAndGet(nowActive, Math::max);
System.out.printf("Task %d entered | active=%d | permitsLeft=%d%n",
taskId, nowActive, semaphore.availablePermits());
// Edge case / failure path: one task throws, but we still must release the permit.
if (taskId == 4) {
throw new IllegalStateException("Simulated failure while holding a permit");
}
Thread.sleep(250L + taskId * 50L);
System.out.printf("Task %d finished work%n", taskId);
} catch (InterruptedException e) {
// Always restore the interrupt flag so higher-level code can notice the cancellation.
Thread.currentThread().interrupt();
System.out.printf("Task %d was interrupted%n", taskId);
} catch (RuntimeException e) {
System.out.printf("Task %d failed: %s%n", taskId, e.getMessage());
} finally {
if (acquired) {
active.decrementAndGet();
semaphore.release();
System.out.printf("Task %d released | permitsLeft=%d%n",
taskId, semaphore.availablePermits());
}
done.countDown();
}
});
}
for (Runnable task : tasks) {
pool.submit(task);
}
done.await();
pool.shutdown();
pool.awaitTermination(2, TimeUnit.SECONDS);
System.out.println("--- Summary ---");
System.out.println("Max concurrent workers observed: " + maxActive.get());
System.out.println("Permits after all tasks: " + semaphore.availablePermits());
// Failure path: tryAcquire can time out instead of blocking forever.
Semaphore empty = new Semaphore(0);
boolean gotPermit = empty.tryAcquire(200, TimeUnit.MILLISECONDS);
System.out.println("tryAcquire on empty semaphore succeeded? " + gotPermit);
}
}
Follow-up & Tricky Questions:
new Semaphore(permits, true) tries to serve waiting threads in arrival order, which reduces starvation but can reduce throughput.acquire() throws InterruptedException, so the code should stop waiting, restore the interrupt flag if appropriate, and not assume a permit was obtained.release()? Yes. That is legal for semaphores, unlike locks, because permits are not owned by a specific thread.tryAcquire(timeout)? When waiting too long is worse than failing fast, such as a UI request or a bounded API call budget.availablePermits() is 1, can I safely proceed? No, because another thread can take that permit immediately after your check; you must acquire atomically.Common Mistakes:
finally: This leaks permits. Fix: release in a finally block even when exceptions happen.Lock or synchronized for mutual exclusion.true only when FIFO-like behavior matters enough to accept lower throughput.acquire() or tryAcquire().Memory Hook: “Semaphore = parking lot tickets.” If you have a ticket, you park; if not, you wait outside until someone leaves.
Cheat Sheet:
acquire() blocks, tryAcquire() can fail fast, release() returns a permit.finally.Practice Tasks:
acquire() with tryAcquire(100, TimeUnit.MILLISECONDS) and see how the failure path changes.