Limiting Concurrency: Semaphore

Part 4 of 8 in Java Concurrency: Deep Dive

A Semaphore caps how many threads can hold a permit at once. This post runs one small Java program, compiled and run for real, that puts more threads against a Semaphore than it has permits, and times when each thread actually requests, acquires, and releases its permit.

Semaphore(2) with 5 threads

SemaphoreDemo creates new Semaphore(2) and starts 5 threads. Each thread prints the time it requests a permit, calls semaphore.acquire(), prints the time it actually got one, holds it for 500ms, then releases and prints that time too.

import java.util.concurrent.Semaphore;

public class SemaphoreDemo {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(2);
        long start = System.nanoTime();
        int threadCount = 5;
        Thread[] threads = new Thread[threadCount];

        for (int i = 0; i < threadCount; i++) {
            final int id = i;
            threads[i] = new Thread(() -> {
                long requestedAt = (System.nanoTime() - start) / 1_000_000;
                System.out.println("thread " + id + " requesting permit at " + requestedAt + "ms");
                try {
                    semaphore.acquire();
                    long acquiredAt = (System.nanoTime() - start) / 1_000_000;
                    System.out.println("thread " + id + " acquired permit at " + acquiredAt + "ms");
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return;
                } finally {
                    semaphore.release();
                    long releasedAt = (System.nanoTime() - start) / 1_000_000;
                    System.out.println("thread " + id + " released permit at " + releasedAt + "ms");
                }
            });
        }

        for (Thread t : threads) t.start();
        for (Thread t : threads) t.join();
    }
}

Real javac+java output, JDK 25 (Zulu):

thread 1 requesting permit at 5ms
thread 3 requesting permit at 5ms
thread 2 requesting permit at 5ms
thread 4 requesting permit at 5ms
thread 0 requesting permit at 5ms
thread 1 acquired permit at 11ms
thread 3 acquired permit at 11ms
thread 2 acquired permit at 512ms
thread 0 acquired permit at 512ms
thread 1 released permit at 511ms
thread 3 released permit at 511ms
thread 4 acquired permit at 1012ms
thread 2 released permit at 1012ms
thread 0 released permit at 1012ms
thread 4 released permit at 1512ms

All 5 threads requested a permit at almost the same instant, 5ms. But only threads 1 and 3 actually acquired one right away, at 11ms. Threads 2 and 0 didn’t acquire until 512ms — the same moment threads 1 and 3 released. Thread 4 didn’t acquire until 1012ms, the moment threads 2 and 0 released. Each thread held its permit for close to 500ms before releasing, and the next pair of threads only got in once a permit actually came free.

Takeaway

With 2 permits and 5 threads all requesting at 5ms, acquisitions came in three waves — 11ms, 512ms, 1012ms — each wave starting right when the previous holders released. The excess threads weren’t just capped in count; they sat blocked on acquire() until a permit was actually available.