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.