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);
ModeOrderingThroughputStarvation risk
Non-fair (default)Unspecified (barging allowed)HigherYes — a thread may wait indefinitely
Fair (true)FIFO — longest waiting acquires firstLowerNo — 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.