import java.util.concurrent.Semaphore;
// Allow at most 3 concurrent accesses
Semaphore semaphore = new Semaphore(3);
// Acquire a permit (blocks if none available)
semaphore.acquire();
try {
// --- critical section: access the shared resource ---
doWork();
} finally {
semaphore.release(); // always release in finally
}
// Non-blocking: try to acquire without waiting
boolean acquired = semaphore.tryAcquire();
if (acquired) {
try {
doWork();
} finally {
semaphore.release();
}
} else {
System.out.println("Resource busy — try again later");
}
// Inspect state
System.out.println("Available permits : " + semaphore.availablePermits());
System.out.println("Queued threads : " + semaphore.getQueueLength());
Always call release() in a finally block. A missed release() permanently reduces the permit count and eventually starves all waiting threads.
By default, Semaphore is non-fair — threads acquire permits in an unspecified order, which gives higher throughput but can starve long-waiting threads. Pass true to the constructor for FIFO ordering.
// Non-fair (default) — better throughput
Semaphore unfair = new Semaphore(5);
// Fair — FIFO ordering; prevents starvation
Semaphore fair = new Semaphore(5, true);
| Mode | Ordering | Throughput | Starvation risk |
| Non-fair (default) | Unspecified (barging allowed) | Higher | Yes — a thread may wait indefinitely |
Fair (true) | FIFO — longest waiting acquires first | Lower | No — guaranteed progress |
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
Semaphore sem = new Semaphore(2);
// Try to acquire with a deadline
boolean got = sem.tryAcquire(500, TimeUnit.MILLISECONDS);
if (got) {
try {
processRequest();
} finally {
sem.release();
}
} else {
// Return 429 Too Many Requests or enqueue for retry
throw new RuntimeException("Service busy — please retry");
}
You can acquire and release more than one permit in a single call. This is useful when a task consumes different amounts of a metered resource (e.g., bandwidth units).
Semaphore bandwidth = new Semaphore(100); // 100 units available
// Acquire 10 units for a large request
bandwidth.acquire(10);
try {
sendLargePayload();
} finally {
bandwidth.release(10);
}
// Acquire 1 unit for a small request
bandwidth.acquire(1);
try {
sendHeartbeat();
} finally {
bandwidth.release(1);
}
A semaphore naturally models a fixed-size connection pool: acquire a permit before borrowing a connection, release when returning it.
import java.util.concurrent.*;
public class ConnectionPool {
private final Semaphore semaphore;
private final BlockingQueue<Connection> pool;
public ConnectionPool(int maxConnections) {
semaphore = new Semaphore(maxConnections, true); // fair
pool = new ArrayBlockingQueue<>(maxConnections);
for (int i = 0; i < maxConnections; i++) {
pool.add(createConnection(i));
}
}
/** Borrow a connection; blocks until one is available. */
public Connection acquire() throws InterruptedException {
semaphore.acquire();
return pool.poll(); // always non-null after acquiring permit
}
/** Borrow with timeout; returns null on timeout. */
public Connection tryAcquire(long timeout, TimeUnit unit)
throws InterruptedException {
if (!semaphore.tryAcquire(timeout, unit)) return null;
return pool.poll();
}
/** Return a connection to the pool. */
public void release(Connection conn) {
pool.offer(conn);
semaphore.release();
}
private Connection createConnection(int id) {
return new Connection("conn-" + id);
}
record Connection(String id) {}
}
A semaphore can implement a simple token-bucket style rate limiter by periodically refilling permits with a background scheduler.
import java.util.concurrent.*;
public class RateLimiter {
private final Semaphore semaphore;
/**
* @param maxRequests maximum requests allowed per interval
* @param interval refill interval
* @param unit time unit for interval
*/
public RateLimiter(int maxRequests, long interval, TimeUnit unit) {
semaphore = new Semaphore(maxRequests);
// Refill permits on a fixed schedule
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
int needed = maxRequests - semaphore.availablePermits();
if (needed > 0) semaphore.release(needed);
}, interval, interval, unit);
}
/** Blocks until a permit is available. */
public void acquire() throws InterruptedException {
semaphore.acquire();
}
/** Non-blocking check. */
public boolean tryAcquire() {
return semaphore.tryAcquire();
}
}
// Usage: allow 100 requests per second
RateLimiter limiter = new RateLimiter(100, 1, TimeUnit.SECONDS);
limiter.acquire();
callExternalApi();
For production rate limiting, prefer libraries like Resilience4j or Guava's RateLimiter which implement token-bucket and leaky-bucket algorithms more accurately. The semaphore approach above is a useful illustration but lacks sub-second precision.
A Semaphore(1) acts as a non-reentrant mutex. Unlike synchronized, it can be released by a different thread — useful for producer-consumer handoffs.
Semaphore mutex = new Semaphore(1);
// Thread A: acquire and hold
mutex.acquire();
System.out.println("Thread A has the lock");
// Thread B: can release (unlike ReentrantLock)
new Thread(() -> {
mutex.release(); // valid even from a different thread
System.out.println("Thread B released the lock");
}).start();
A binary Semaphore is not reentrant — the same thread calling acquire() twice will deadlock itself. Use ReentrantLock when reentrancy is required.