Understanding and Analyzing the Implementation of Java's Semaphore
This article explains the internal workings of Java's Semaphore, detailing its AQS-based architecture, fair and non‑fair synchronization strategies, core methods such as acquire, release, and their implementations, and provides a practical example demonstrating semaphore usage for thread coordination.
Java's Semaphore is a synchronization aid that differs from CountDownLatch and CyclicBarrier because its internal counter can be increased as well as decreased. The implementation relies on the AbstractQueuedSynchronizer (AQS) framework, with a nested Sync class that has two concrete subclasses to support fair and non‑fair acquisition policies.
The basic constructors illustrate how the fairness flag determines which Sync implementation is used:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}Both constructors store the initial permit count in the AQS state variable, which represents the number of available permits.
The core acquisition methods delegate to the synchronizer:
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}acquire() obtains a single permit, while acquire(int) requests a specific number. Both methods may block the calling thread and respond to interruption.
Internally, Sync.acquireSharedInterruptibly calls tryAcquireShared . In the non‑fair implementation, tryAcquireShared simply forwards to nonfairTryAcquireShared , which repeatedly reads the current state, computes the remaining permits, and attempts a CAS update:
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 || compareAndSetState(available, remaining))
return remaining;
}
}If the remaining permits are negative, the method returns a negative value, causing the thread to be placed on the AQS wait queue.
The fair variant adds an extra check using hasQueuedPredecessors() to ensure that threads acquire permits in FIFO order:
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 || compareAndSetState(available, remaining))
return remaining;
}
}The hasQueuedPredecessors method inspects the AQS queue to determine whether the current thread has any waiting predecessors.
Releasing permits works similarly but in the opposite direction. The simple release() method adds one permit and wakes up a waiting thread if any:
public void release() {
sync.releaseShared(1);
}
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow check
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}A variant release(int permits) adds the specified number of permits.
To illustrate usage, the article provides a complete example that creates a Semaphore with zero initial permits, starts two worker threads that each call release() , and then blocks the main thread with acquire(2) until both releases have occurred:
public class SemaphoreTest {
private static volatile Semaphore semaphore = new Semaphore(0);
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> {
System.out.println(Thread.currentThread() + " over");
semaphore.release();
});
executor.submit(() -> {
System.out.println(Thread.currentThread() + " over");
semaphore.release();
});
semaphore.acquire(2);
System.out.println("all child thread over!");
executor.shutdown();
}
}The program prints the two worker thread messages followed by the final message once both permits have been released.
Finally, the article compares Semaphore with CountDownLatch and CyclicBarrier :
CountDownLatch uses a countdown that cannot be reset, making it suitable for one‑time synchronization.
CyclicBarrier can be reused after calling reset() , fitting scenarios where the same barrier is needed repeatedly.
Semaphore provides a flexible permit‑based mechanism with optional fairness, allowing dynamic control over how many threads may proceed.
Overall, the article offers a deep dive into the source code of Java's semaphore, clarifying how permits are managed, how fairness is enforced, and how the class can be applied in real‑world concurrent programs.
Architect's Tech Stack
Java backend, microservices, distributed systems, containerized programming, and more.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.