Using Semaphores for Resource Limiting in Java: A Practical Guide
Learn how to use Java semaphores to limit concurrent resource access. This guide covers creating a semaphore, acquiring/releasing permits, handling timeouts, and verifying correct behavior with a concrete example.
27 Mar 2026, 23:10 UTC

Desired Outcome
The goal is to add a lightweight, thread‑safe mechanism that limits how many threads can access a shared resource at the same time. A semaphore lets you specify a fixed number of permits; each thread must acquire a permit before proceeding and release it afterward. This pattern is common for connection pools, rate limiting, and any scenario where you want to avoid over‑utilization of a bounded resource.
Prerequisites
- Java Development Kit (JDK) 17 or newer installed and available on the PATH.
- Basic understanding of threads and the
java.util.concurrentpackage. - Access to a terminal or command‑line interface with permission to compile and run Java code.
Focused Procedure
- Create the semaphore instance
Decide how many permits you need. For a pool of 2 database connections, you would create a semaphore with 2 permits. Optionally, pass
truefor the fairness flag to ensure first‑come, first‑served ordering.import java.util.concurrent.Semaphore; public class ResourcePool { private final Semaphore permits = new Semaphore(2, true); // 2 permits, fair ordering // ... } - Acquire a permit before accessing the resource
Use
acquire()for a blocking call,tryAcquire()for non‑blocking, ortryAcquire(long timeout, TimeUnit unit)for a timed attempt. HandleInterruptedExceptionappropriately.try { permits.acquire(); // blocks until a permit is available // perform work with the shared resource } catch (InterruptedException e) { Thread.currentThread().interrupt(); // handle interruption, e.g., abort the operation } finally { permits.release(); // always release to avoid deadlock } - Release the permit after use
Placing
release()in afinallyblock guarantees that the permit is returned even if the work throws an exception. - Optional: Inspect semaphore state
For debugging or monitoring, you can query
availablePermits()to see how many permits remain, orhasQueuedThreads()to check if any threads are waiting.System.out.println("Permits left: " + permits.availablePermits());
Concrete Example
Below is a minimal program that starts five threads, each trying to acquire a semaphore with two permits. The program logs when a thread acquires and releases a permit, demonstrating that only two threads can run the critical section concurrently.
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreDemo {
private static final Semaphore semaphore = new Semaphore(2, true);
public static void main(String[] args) {
for (int i = 1; i <= 5; i++) {
new Thread(new Task(i)).start();
}
}
static class Task implements Runnable {
private final int id;
Task(int id) { this.id = id; }
@Override
public void run() {
try {
System.out.println("Thread " + id + " waiting for permit");
if (semaphore.tryAcquire(5, TimeUnit.SECONDS)) {
System.out.println("Thread " + id + " acquired permit");
// Simulate work
Thread.sleep(2000);
System.out.println("Thread " + id + " releasing permit");
} else {
System.out.println("Thread " + id + " timed out waiting for permit");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release();
}
}
}
}
Compile and Run
Open a terminal, navigate to the directory containing SemaphoreDemo.java, and run:
# Compile
javac SemaphoreDemo.java
# Execute
java SemaphoreDemo
Expected output (order may vary but only two "acquired" lines should appear concurrently):
Thread 1 waiting for permit
Thread 2 waiting for permit
Thread 1 acquired permit
Thread 2 acquired permit
Thread 3 waiting for permit
Thread 4 waiting for permit
Thread 5 waiting for permit
Thread 1 releasing permit
Thread 2 releasing permit
Thread 3 acquired permit
Thread 4 acquired permit
Thread 3 releasing permit
Thread 4 releasing permit
Thread 5 acquired permit
Thread 5 releasing permit
Expected Checks
- Only two threads should print "acquired" at the same time. If more do, the semaphore count was misconfigured.
- All threads should eventually release a permit; otherwise, a deadlock will occur.
- If a thread times out, it should not attempt to release a permit that was never acquired. The
finallyblock must guard against this by checking the return value oftryAcquirebefore callingrelease.
Recovery Options
- Timeouts: Use
tryAcquire(long timeout, TimeUnit)to avoid indefinite blocking. If a timeout occurs, log the event and skip the operation or retry later. - Interruptibility: If the thread may be interrupted (e.g., during shutdown), use
acquireInterruptibly()and propagate the interruption to the caller. - Fairness: If starvation is observed, enable the fairness flag when constructing the semaphore. Note that fairness can reduce throughput due to context switching overhead.
- Resource cleanup: In a long‑running application, expose a shutdown hook that releases all permits or shuts down the thread pool gracefully.
Limitations & Practical Checks
- Fairness is optional; the default non‑fair mode may allow thread reordering, potentially leading to starvation of older waiters.
- Semaphores do not protect against misuse such as calling
release()more times thanacquire()—this will increase the permit count beyond the initial value. Always pair acquire/release in the same thread context. - Testing for deadlock: Run the program under a profiler or use
jcmdto inspect thread states. A thread stuck inWAITINGfor a permit while all permits are held indicates a potential issue. - Cross‑platform consistency: While Java’s semaphore API is stable across JDKs, older runtimes may not support timed or interruptible acquire methods. Verify the JDK version and consult the
java.util.concurrentdocumentation for your target environment.
Summary
Semaphores provide a straightforward, language‑level solution to limit concurrent access to a bounded resource. By carefully acquiring and releasing permits, adding timeouts, and enabling fairness when needed, you can prevent resource exhaustion while maintaining responsiveness. Always validate the behavior with a small test harness before scaling to production workloads.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.