Back to Blog
Java

Using Semaphore in Java for Concurrency Control

java semaphore: Learn how to use java.util.concurrent.Semaphore to control access to shared resources, handle fairness, avoid common pitfalls, and choose the right syn...

concurrencythread-safetysynchronizationmultithreadingjava.util.concurrent
Illustration of a Java semaphore controlling access to a shared resource with multiple threads waiting for permits.

When multiple threads need to access a limited resource, a java semaphore provides a straightforward way to cap concurrent access. Unlike a lock that allows only one thread at a time, a semaphore maintains a set of permits. Threads acquire a permit before entering a critical section and release it when done. This makes semaphores suitable for rate limiting, connection pooling, and throttling tasks.

How a Semaphore Works

The Semaphore class in java.util.concurrent is initialized with a fixed number of permits. Each call to acquire() blocks until a permit is available, and each call to release() returns a permit. The internal counter is not tied to a specific thread, so a permit acquired by one thread can be released by another. That flexibility is useful, but it also means you must be careful about ownership.

import java.util.concurrent.Semaphore; Semaphore semaphore = new Semaphore(3); // Acquire a permit (blocks if none available) semaphore.acquire(); try { // access the shared resource } finally { semaphore.release(); }

The try-finally block is essential. If an exception occurs inside the critical section, the permit is still released, preventing the semaphore from leaking permits over time.

Acquiring Permits with Timeouts and Interrupts

Blocking indefinitely on acquire() is not always acceptable. The tryAcquire overloads allow you to wait for a bounded time or return immediately if no permit is available. This is useful when you want to fail fast or degrade gracefully under load.

if (semaphore.tryAcquire(2, TimeUnit.SECONDS)) { try { // perform the operation } finally { semaphore.release(); } } else { // handle the timeout }

acquire() and tryAcquire(long, TimeUnit) throw InterruptedException when the waiting thread is interrupted. You need to handle this exception, either by propagating it or by restoring the interrupt status. Ignoring it can leave the thread in an inconsistent state.

Fairness and Thread Ordering

The Semaphore constructor accepts a boolean fair parameter. A fair semaphore guarantees that threads acquire permits in the order they requested them (FIFO). An unfair semaphore may allow barging, where a newly arriving thread can grab a permit before a thread that has been waiting longer. Unfair semaphores generally have higher throughput because they reduce context switching, but they can cause starvation in extreme cases.

Semaphore fairSemaphore = new Semaphore(1, true); Semaphore unfairSemaphore = new Semaphore(1);

Use a fair semaphore when you need predictable ordering, such as in a queue where each task should be served in arrival order. Use an unfair semaphore when throughput is more important than ordering and the critical section is short.

Common Pitfalls with Semaphores

One frequent mistake is releasing a permit that was never acquired. This increases the permit count beyond the initial value, allowing more threads than intended to enter the critical section. Another pitfall is acquiring multiple permits without releasing them individually. If you call acquire(2), you must call release(2) to restore the correct count.

// Wrong: releasing without acquiring semaphore.release(); // increases permits beyond initial value // Correct: acquire and release the same number semaphore.acquire(2); try { // critical section } finally { semaphore.release(2); }

Also, remember that a semaphore is not a reentrant lock. If a thread tries to acquire the same semaphore again while holding a permit, it will deadlock unless the semaphore has more than one permit. This differs from ReentrantLock, which allows the same thread to reacquire the lock.

Semaphore vs Other Synchronization Primitives

Choosing between a semaphore, a lock, and a CountDownLatch depends on the concurrency pattern you need.

PrimitivePurposeKey Characteristic
SemaphoreLimit concurrent accessPermits can be acquired and released by different threads
ReentrantLockMutual exclusion with reentrancyOnly one thread holds the lock; owner-based
CountDownLatchWait for a set of events to completeCounts down to zero; cannot be reset

Use a semaphore when you need to allow multiple threads into a resource but cap the total number. Use a lock when you need exclusive access with reentrant behavior. Use a CountDownLatch when you need to wait for several operations to finish before proceeding.

Performance and Contention Considerations

Semaphores rely on the same underlying AQS (AbstractQueuedSynchronizer) machinery as other java.util.concurrent classes. The cost of acquiring and releasing a permit is low when there is no contention. Under contention, threads may be suspended and resumed, which involves OS-level context switching. A fair semaphore tends to have higher overhead because it enforces strict ordering. If your critical section is very short, an unfair semaphore often performs better.

Another performance concern is the number of permits. Setting it too high defeats the purpose of limiting concurrency; setting it too low can underutilize the resource. Monitor the actual concurrency and adjust based on observed behavior rather than guessing.

Using Semaphore in a Realistic Scenario

A typical use case is a connection pool where you want to limit the number of simultaneous database connections. The semaphore controls access to a shared pool object.

public class ConnectionPool { private final Semaphore semaphore; private final List<Connection> connections; public ConnectionPool(int poolSize) { semaphore = new Semaphore(poolSize, true); connections = new ArrayList<>(poolSize); // initialize connections } public Connection getConnection() throws InterruptedException { semaphore.acquire(); return connections.remove(0); } public void returnConnection(Connection conn) { connections.add(conn); semaphore.release(); } }

This implementation assumes the pool is always initialized with enough connections. In production, you would also handle cases where the pool is empty or connections are invalid. The key point is that the semaphore enforces the maximum number of borrowed connections, and each returnConnection call releases a permit so another thread can borrow.

Handling Exceptions and Interruptions Gracefully

When a thread is interrupted while waiting for a permit, acquire() throws InterruptedException. The standard practice is to restore the interrupt flag and let the caller decide how to respond. This is especially important in long-running applications where a thread may be cancelled by a shutdown mechanism.

public void run() { try { semaphore.acquire(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; // or handle cancellation } try { // critical section } finally { semaphore.release(); } }

Failing to restore the interrupt status can cause higher-level code to miss the interruption, leading to unresponsive threads. Always propagate or restore the interrupt flag unless you have a specific reason not to.

When a Semaphore Is the Wrong Choice

A semaphore is not a substitute for a lock when you need mutual exclusion with reentrancy. It also does not provide the same guarantees as a ReadWriteLock for reader-writer scenarios. If you need to protect a single shared variable, a simple synchronized block or AtomicInteger is often simpler and less error-prone. Semaphores shine when the resource is a pool or a bounded queue, and the number of concurrent users must be limited independently of thread ownership.

For example, if you want to limit the number of concurrent HTTP requests to an external API, a semaphore with a fixed number of permits is a clean solution. But if you need to ensure that a block of code is executed by only one thread at a time, and that same thread may re-enter the block, use a ReentrantLock instead.

java semaphore: Practical Usage and Code Examples | RYUSLOG DEV