Fundamentals 13 min read

Understanding Java Semaphore: Usage Scenarios, Code Example, and Source‑Code Deep Dive

This article introduces Java's Semaphore as a thread‑synchronization tool, explains its flow‑control use cases such as database connections and parking lots, provides a complete parking‑lot simulation code sample, and analyzes the underlying AQS‑based implementation including fairness, acquire/release mechanics, and auxiliary methods.

Full-Stack Internet Architecture
Full-Stack Internet Architecture
Full-Stack Internet Architecture
Understanding Java Semaphore: Usage Scenarios, Code Example, and Source‑Code Deep Dive

This is the second article in a series on concurrent thread utilities; the first covered CountDownLatch (see the linked article for details). Here we continue with Semaphore , a synchronization primitive that controls access to shared resources by managing a set of permits.

Getting to Know Semaphore

What Is a Semaphore?

Semaphore (often translated as "signal" or "permit") represents a count of permits that determine whether multiple threads may concurrently operate on the same resource. By acquiring a permit a thread is allowed to proceed; releasing a permit makes it available for others.

Typical Usage Scenarios

Semaphores are commonly used for flow control, for example limiting the number of simultaneous database connections, controlling traffic lights, or managing parking‑lot capacity where cars must wait when no spots are free.

Semaphore Usage Example

Below is a simple simulation of a parking‑lot scenario. A sign shows the remaining spots; when the count reaches zero, incoming cars must wait. The code creates a semaphore with 10 permits (10 parking spots) and spawns 100 car threads.

public class CarParking {
    private static Semaphore semaphore = new Semaphore(10);

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            Thread thread = new Thread(() -> {
                System.out.println("Welcome " + Thread.currentThread().getName() + " to the parking lot");
                if (semaphore.availablePermits() == 0) {
                    System.out.println("No spots, please wait");
                }
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + " entered the parking lot");
                    Thread.sleep(new Random().nextInt(10000)); // simulate parking time
                    System.out.println(Thread.currentThread().getName() + " left the parking lot");
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, i + "th car");
            thread.start();
        }
    }
}

The program sets the initial capacity to 10 permits, so only ten cars can be inside at any moment; the remaining 90 cars wait in the queue, which matches the expected behavior.

Semaphore Model Diagram

The following image illustrates the internal model of a semaphore (permits, acquire, release, and waiting queue).

When acquire() is called, the permit count decreases by one; when release() is called, it increases by one. If the count would drop below zero, the thread is placed into a waiting queue until a permit becomes available.

Semaphores actually control a set of threads, because the primary research object of concurrency utilities is the thread.

The workflow is visualized in the next diagram.

Deep Dive into Semaphore Implementation

After covering basic usage, we now examine the source code to understand the inner workings of java.util.concurrent.Semaphore . The class contains a single field:

private final Sync sync;

Sync is the synchronization core, extending AbstractQueuedSynchronizer (AQS) , just like ReentrantLock and CountDownLatch . The Sync class defines the state (the permit count) and implements the core acquire/release logic.

Fairness vs. Non‑fairness

Semaphore supports both fair and non‑fair modes. By default it is non‑fair: a thread calling acquire() may obtain a permit even if other threads are already waiting. In fair mode (constructed with new Semaphore(permits, true) ), threads acquire permits in FIFO order.

The key difference lies in the implementation of tryAcquireShared inside FairSync and NonfairSync . The non‑fair version ignores the waiting queue and attempts a CAS update directly; the fair version first checks the queue and fails if there are waiting threads.

Core Methods in Sync

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 1192457210091910933L;
    Sync(int permits) { setState(permits); }
    final int getPermits() { return getState(); }
    final int nonfairTryAcquireShared(int acquires) { ... }
    protected final boolean tryReleaseShared(int releases) { ... }
    final void reducePermits(int reductions) { ... }
    final int drainPermits() { ... }
}

The constructor sets the initial state (permit count). getPermits() simply returns the current state. nonfairTryAcquireShared attempts to decrement the state atomically; if the result would be negative, acquisition fails and the thread is queued. tryReleaseShared increments the state and wakes up waiting threads.

Other public methods of Semaphore simply delegate to these low‑level operations:

drainPermits() – atomically removes all available permits.

reducePermits(int) – permanently lowers the number of permits.

isFair() – reports whether the semaphore was created in fair mode.

hasQueuedThreads() , getQueuedThreads() , getQueueLength() – expose the waiting queue state.

Understanding these details helps avoid pitfalls such as using acquireUninterruptibly() , which can cause a thread pool to deadlock if permits are never released.

Additional Notes

Both acquire() (blocking) and tryAcquire() (non‑blocking) ultimately rely on tryAcquireShared . The blocking version may invoke doAcquireSharedInterruptibly to wait until a permit becomes available.

Remember: acquire() blocks until a permit is granted, while tryAcquire() returns immediately with a boolean result.

With this comprehensive overview, readers should now be able to use Semaphore effectively, understand its internal AQS‑based implementation, and choose the appropriate fairness policy for their concurrency scenarios.

JavaconcurrencySemaphorefairnessAQSThread Synchronization
Full-Stack Internet Architecture
Written by

Full-Stack Internet Architecture

Introducing full-stack Internet architecture technologies centered on Java

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.