What is a Semaphore?
A semaphore is a concurrency primitive that controls access to a shared resource by maintaining a set of permits. Threads acquire permits before accessing the resource and release them when done. Unlike locks, semaphores don’t have an “owner” — any thread can release a permit, not just the one that acquired it.

Key Difference: Semaphore vs Lock
| Aspect | Lock/Mutex | Semaphore |
|---|---|---|
| Permits | Exactly 1 (exclusive) | N (configurable) |
| Ownership | Thread that locked must unlock | Any thread can release |
| Reentrancy | Supported (ReentrantLock) | Not applicable |
| Purpose | Mutual exclusion | Resource limiting / signaling |
| Analogy | Key to a room (one person) | Parking lot (N spaces) |
Basic Usage
import java.util.concurrent.Semaphore;
public class ConnectionPool {
private final Semaphore semaphore;
private final List<Connection> connections;
public ConnectionPool(int poolSize) {
this.semaphore = new Semaphore(poolSize);
this.connections = createConnections(poolSize);
}
public Connection acquire() throws InterruptedException {
semaphore.acquire(); // Block until a permit is available
return getAvailableConnection();
}
public void release(Connection conn) {
returnConnectionToPool(conn);
semaphore.release(); // Return the permit
}
}
Semaphore API
Core Methods
| Method | Description |
|---|---|
acquire() | Acquires one permit. Blocks if none available. |
acquire(int n) | Acquires n permits at once. |
tryAcquire() | Tries to acquire without blocking. Returns boolean. |
tryAcquire(long timeout, TimeUnit unit) | Tries to acquire with a timeout. |
release() | Releases one permit. |
release(int n) | Releases n permits. |
availablePermits() | Returns the number of currently available permits. |
drainPermits() | Acquires all available permits and returns the count. |
Informational Methods
| Method | Description |
|---|---|
hasQueuedThreads() | Are threads waiting to acquire? |
getQueueLength() | Estimated number of waiting threads. |
isFair() | Was the semaphore created with fairness enabled? |
Fair vs Unfair Semaphore
Semaphore unfair = new Semaphore(5); // Default: unfair
Semaphore fair = new Semaphore(5, true); // Fair: FIFO guarantee
Fair semaphores guarantee that threads acquire permits in the order they requested them. This prevents starvation but reduces throughput.
Pattern: Rate Limiter (Fixed Window)
Limit the number of concurrent operations:
public class RateLimiter {
private final Semaphore semaphore;
public RateLimiter(int maxConcurrent) {
this.semaphore = new Semaphore(maxConcurrent);
}
public <T> T execute(Callable<T> task) throws Exception {
semaphore.acquire();
try {
return task.call();
} finally {
semaphore.release();
}
}
}
// Usage: max 10 concurrent API calls
RateLimiter limiter = new RateLimiter(10);
String result = limiter.execute(() -> callExternalApi());
Pattern: Producer-Consumer Signaling
Use two semaphores to coordinate a bounded buffer without explicit locks:
public class SemaphoreBuffer<T> {
private final Queue<T> queue = new ConcurrentLinkedQueue<>();
private final Semaphore availableItems; // Signals consumers: items ready
private final Semaphore availableSlots; // Signals producers: space ready
public SemaphoreBuffer(int capacity) {
availableItems = new Semaphore(0); // Initially: no items
availableSlots = new Semaphore(capacity); // Initially: all slots free
}
public void put(T item) throws InterruptedException {
availableSlots.acquire(); // Wait for free slot
queue.add(item);
availableItems.release(); // Signal that an item is available
}
public T take() throws InterruptedException {
availableItems.acquire(); // Wait for available item
T item = queue.poll();
availableSlots.release(); // Signal that a slot is free
return item;
}
}
This is elegant: two semaphores signal each other — availableSlots tells producers when to go, availableItems tells consumers when to go. No explicit locking of the queue is needed because ConcurrentLinkedQueue is thread-safe.
Pattern: Binary Semaphore (Mutex)
A semaphore with one permit behaves like a non-reentrant mutex:
Semaphore mutex = new Semaphore(1);
mutex.acquire();
try {
// Exclusive access — only one thread at a time
} finally {
mutex.release();
}
Unlike ReentrantLock, a binary semaphore is not reentrant — acquiring twice from the same thread will deadlock.
TimedSemaphore (Apache Commons)
A semaphore that automatically resets its permits at regular intervals:
import org.apache.commons.lang3.concurrent.TimedSemaphore;
// Allow max 100 operations per second
TimedSemaphore rateLimiter = new TimedSemaphore(1, TimeUnit.SECONDS, 100);
public void handleRequest() throws InterruptedException {
rateLimiter.acquire(); // Blocks if 100 operations already happened this second
processRequest();
// No release needed — permits reset automatically every second
}
Use Cases for TimedSemaphore
| Use Case | Configuration |
|---|---|
| API rate limiting (100 req/sec) | TimedSemaphore(1, SECONDS, 100) |
| Email throttling (50 emails/min) | TimedSemaphore(1, MINUTES, 50) |
| Database query limiting | TimedSemaphore(1, SECONDS, 20) |
Note: TimedSemaphore implements a fixed-window rate limiter. For sliding-window or token-bucket algorithms, a different approach is needed.
Semaphore vs Other Synchronization

Common Mistakes
1. Forgetting to Release
// WRONG — if processRequest() throws, permit is never released
semaphore.acquire();
processRequest();
semaphore.release();
// CORRECT — always release in finally
semaphore.acquire();
try {
processRequest();
} finally {
semaphore.release();
}
2. Releasing Without Acquiring
// WRONG — release without acquire inflates the permit count
semaphore.release(); // Now permits = maxPermits + 1!
Semaphore doesn’t track ownership, so it won’t throw. The permit count will silently grow beyond the intended maximum.
3. Using Semaphore as a Lock
// WRONG — semaphore is not reentrant
Semaphore mutex = new Semaphore(1);
void methodA() {
mutex.acquire();
methodB(); // DEADLOCK — same thread tries to acquire again
mutex.release();
}
void methodB() {
mutex.acquire(); // Blocks forever — semaphore has no reentrant concept
// ...
mutex.release();
}
Summary
- Semaphores limit the number of threads accessing a resource concurrently.
- Unlike locks, they have no ownership — any thread can release.
- Use for connection pools, rate limiting, and producer-consumer signaling.
- Always release in a
finallyblock. - A semaphore with 0 initial permits is a useful signaling mechanism between threads.
- TimedSemaphore auto-resets permits at intervals — useful for simple rate limiting.