A Semaphore controls how many threads can access a shared resource at the same time.
Problem description:
We need to limit concurrency against a shared dependency or scarce resource instead of letting unlimited requests pile in.
What we are solving actually:
We are solving admission control, not scheduling. The semaphore decides how many callers may proceed at once and forces the rest to wait or fail fast.
What we are doing actually:
- Create a semaphore with a permit count equal to safe concurrency.
- Acquire before entering the protected work.
- Release in
finallyso permits are never leaked.
flowchart LR
A[Incoming task] --> B{Permit available?}
B -->|Yes| C[Acquire permit]
B -->|No| D[Wait or timeout]
C --> E[Use dependency]
E --> F[Release permit]
Real-World Use Cases
- limiting concurrent outbound API calls
- database connection throttling
- download/upload slot management
- rate-limited integrations
Core Concept
A semaphore has permits:
acquire()takes a permit (waits if none available)release()returns a permit
You can also choose fairness:
Semaphore fair = new Semaphore(10, true);
Fair semaphores reduce starvation risk, but may have lower throughput.
Java 8 Style Example
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
private static final Semaphore semaphore = new Semaphore(2);
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
final int id = i;
new Thread(() -> runTask(id), "worker-" + id).start();
}
}
private static void runTask(int id) {
try {
semaphore.acquire();
System.out.println("Task " + id + " acquired permit");
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release();
System.out.println("Task " + id + " released permit");
}
}
}
Timeout Pattern (tryAcquire)
In request paths, blocking forever is dangerous. Prefer timeout-based acquisition.
if (!semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("dependency busy, please retry");
}
try {
callDownstream();
} finally {
semaphore.release();
}
This gives predictable backpressure behavior under saturation.
Bulkhead Pattern with Semaphore
A common production pattern is one semaphore per downstream dependency:
paymentApiSemaphorefor payment providersearchApiSemaphorefor search dependencyemailApiSemaphorefor notifications
This prevents one slow dependency from consuming all worker capacity.
Permit Leak Prevention
Permit leaks are serious: eventually all requests block.
Checklist:
- release only if acquisition succeeded
- use
finallyalways - avoid branching paths that skip release
- track available permits and waiting threads in metrics
JDK 11 and Java 17 Notes
Semaphore API is stable in JDK 11 and Java 17. The same concurrency-limit pattern is still the recommended approach.
Java 21+ Example (Virtual Threads)
try (var executor = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor()) {
Semaphore semaphore = new Semaphore(50);
for (int i = 0; i < 10_000; i++) {
int id = i;
executor.submit(() -> {
semaphore.acquire();
try {
// remote call / IO task
} finally {
semaphore.release();
}
return id;
});
}
}
This is useful when you run many lightweight tasks but still need strict external resource limits.
Java 25 Note
Semaphore API remains stable. The same design works; focus on observability and backpressure instead of API migration.
Monitoring Checklist
availablePermits()trend- acquire timeout count
- average wait time before acquire
- request failure rate during saturation
Debug steps:
- log acquisition failures and timeouts separately from downstream failures
- verify permits are released only after successful acquisition
- compare
availablePermits()trends with real downstream saturation - use fairness only when starvation risk matters more than throughput
These signals tell you whether permit counts match real dependency capacity.
Key Takeaways
- Semaphore is a concurrency limiter, not a queue.
- Always release permits in
finally. - Combine semaphores with retry and timeout logic in production systems.
- Prefer timeout-based acquisition on latency-sensitive paths.
Comments