☕ Java

Semaphore

A Semaphore maintains a set of permits. Threads acquire permits before proceeding and release them when done, allowing at most N threads to access a resource simultaneously. A semaphore with N=1 behaves as a mutual exclusion lock — a binary semaphore — but unlike a lock, it has no owner: any thread can release a permit regardless of which thread acquired it. This non-ownership property makes semaphores suitable for producer-consumer signaling, where the producer acquires and the consumer releases, or vice versa. java.util.concurrent.Semaphore supports both fair and unfair permit distribution, timed and interruptible acquisition, and bulk acquire/release. Common applications include connection pools (limit simultaneous database connections), rate limiters (limit requests per time window), and bounded resources (limit concurrent file handles, API calls, or memory-intensive computations). This entry covers the full Semaphore API, the fair vs unfair acquisition contract, the non-ownership property and its applications, bulk operations, timed and interruptible acquisition, and canonical usage patterns.

Semaphore API — Permits, Acquisition, and Release

Semaphore is constructed with an initial permit count and an optional fairness flag: new Semaphore(N) (unfair) or new Semaphore(N, true) (fair). The permit count represents the number of threads that may simultaneously hold permits. With N=5, at most 5 threads can acquire permits; a 6th thread blocks until one of the 5 releases. acquire() acquires one permit, blocking if none are available. It responds to interruption: if the waiting thread is interrupted, acquire() throws InterruptedException. acquire(int permits) acquires multiple permits atomically — it blocks until all requested permits are available simultaneously. tryAcquire() attempts to acquire one permit without blocking, returning true if a permit was available and false otherwise. tryAcquire(int permits) is the bulk non-blocking form. tryAcquire(long timeout, TimeUnit unit) waits up to the specified duration. tryAcquire(int permits, long timeout, TimeUnit unit) combines both. release() releases one permit, potentially waking a blocked acquirer. release(int permits) releases multiple permits at once. Crucially: release() can be called by any thread — not just the one that called acquire(). This is the fundamental difference from a lock: semaphores have no owner concept. A thread can acquire a permit and a different thread can release it. This enables signaling patterns impossible with locks. availablePermits() returns the current number of available permits. This is a best-effort snapshot — the value may change immediately after the call. drainPermits() atomically acquires all available permits and returns the count, leaving the semaphore at zero — useful for resetting a semaphore or implementing try-acquire-all patterns. reducePermits(int reduction) decreases the permit count, potentially making it negative, which is used to permanently reduce the resource limit without blocking current holders.
Java
// ── Construction and basic acquire/release ────────────────────────────
Semaphore sem = new Semaphore(3);   // 3 permits: at most 3 concurrent holders

// acquire — blocks if no permits available, throws InterruptedException:
sem.acquire();
try {
    doWork();
} finally {
    sem.release();   // always release in finally — any thread can release
}

// tryAcquire — non-blocking:
if (sem.tryAcquire()) {
    try {
        doWork();
    } finally {
        sem.release();
    }
} else {
    System.out.println("No permit available — skipping or queuing");
}

// tryAcquire with timeout:
try {
    if (sem.tryAcquire(500, TimeUnit.MILLISECONDS)) {
        try {
            doWork();
        } finally {
            sem.release();
        }
    } else {
        System.out.println("Timed out — no permit within 500ms");
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

// ── Bulk acquire/release ──────────────────────────────────────────────
Semaphore bulk = new Semaphore(10);
bulk.acquire(3);    // acquires 3 permits atomically — blocks until all 3 available
try {
    doWorkRequiring3Resources();
} finally {
    bulk.release(3);   // releases all 3 at once
}

// ── availablePermits and drainPermits ────────────────────────────────
Semaphore pool = new Semaphore(5);
System.out.println("Available: " + pool.availablePermits());  // 5
pool.acquire(2);
System.out.println("Available: " + pool.availablePermits());  // 3

int drained = pool.drainPermits();   // atomically takes all 3 remaining permits
System.out.println("Drained: "  + drained);                   // 3
System.out.println("Available: "+ pool.availablePermits());   // 0

pool.release(2);  // release the 2 we acquired — does NOT restore drained ones
System.out.println("Available: "+ pool.availablePermits());   // 2

// ── Non-ownership: different threads acquire and release ───────────────
Semaphore signal = new Semaphore(0);   // starts at 0 — producer must signal first

Thread consumer = new Thread(() -> {
    try {
        System.out.println("Consumer: waiting for item");
        signal.acquire();              // blocks at 0 — waits for producer
        System.out.println("Consumer: item received, processing");
    } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
});

Thread producer = new Thread(() -> {
    System.out.println("Producer: producing item");
    produceItem();
    signal.release();                  // different thread releases — no ownership constraint
    System.out.println("Producer: signalled consumer");
});

consumer.start();
Thread.sleep(100);
producer.start();
consumer.join(); producer.join();
// Consumer: waiting for item
// Producer: producing item
// Producer: signalled consumer
// Consumer: item received, processing

Connection Pool and Rate Limiter Patterns

A connection pool limits the number of simultaneous connections to a shared resource such as a database, external API, or file handle. Without a limit, a burst of concurrent requests might open thousands of connections, overwhelming the resource. A Semaphore with N permits enforces the limit: a thread must acquire a permit before using a connection and releases the permit when done. Threads that cannot acquire a permit block until one is available or time out. The connection pool pattern combines a Semaphore with a pool of pre-created connections (or a lazy creation policy). The Semaphore controls access count; the pool manages the actual connection objects. A thread acquires a permit, then takes a connection from the pool; when done, it returns the connection to the pool and releases the permit. If the pool and semaphore are kept in sync (same N), the system is self-consistent. If they can go out of sync (e.g., a connection is discarded due to error), the semaphore permit can be released without returning a valid connection, causing the next acquirer to create a new connection. A rate limiter controls the number of operations performed within a time window. A simple Semaphore-based rate limiter maintains N permits representing N operations per interval. A background thread periodically calls release(N) to refill the permits, allowing the next N operations to proceed. This is a token bucket algorithm: permits are tokens that accumulate (up to a maximum) and are consumed by operations. Acquiring a permit without blocking means the operation proceeds immediately; blocking means the operation waits for the next refill. The non-ownership property enables both patterns: in connection pools, a thread that encounters an error can release its permit without returning the connection (and the pool handles the missing connection separately); in rate limiters, a dedicated refill thread releases permits that no specific thread owns.
Java
// ── Connection pool using Semaphore ──────────────────────────────────
public class ConnectionPool {
    private final Semaphore       available;
    private final Queue<Connection> pool      = new ConcurrentLinkedQueue<>();
    private final int             maxConns;

    public ConnectionPool(int maxConnections) {
        this.maxConns  = maxConnections;
        this.available = new Semaphore(maxConnections, true);  // fair: no starvation
        // Pre-populate the pool:
        for (int i = 0; i < maxConnections; i++) {
            pool.offer(createConnection());
        }
    }

    public Connection acquire() throws InterruptedException {
        available.acquire();           // blocks if all maxConns permits are out
        Connection conn = pool.poll();
        if (conn == null) {
            // Should not happen if pool and semaphore are in sync, but be defensive:
            available.release();
            throw new IllegalStateException("Pool inconsistency — no connection available");
        }
        return conn;
    }

    public Connection acquire(long timeout, TimeUnit unit) throws InterruptedException {
        if (!available.tryAcquire(timeout, unit)) return null;  // timed out
        Connection conn = pool.poll();
        if (conn == null) { available.release(); return null; }
        return conn;
    }

    public void release(Connection conn) {
        if (conn == null) { available.release(); return; }  // broken conn: release permit only
        pool.offer(conn);    // return connection to pool
        available.release(); // release permit — next acquirer can proceed
    }

    public int available() { return available.availablePermits(); }
    private Connection createConnection() { return new Connection(); }
}

// Usage:
ConnectionPool pool = new ConnectionPool(10);
try (Connection conn = pool.acquireAutoCloseable()) {
    conn.executeQuery("SELECT 1");
}   // Connection auto-released — AutoCloseable pattern below

// AutoCloseable wrapper for try-with-resources:
public AutoCloseable acquireAutoCloseable() throws InterruptedException {
    Connection conn = acquire();
    return () -> release(conn);
}

// ── Rate limiter: token bucket via Semaphore ──────────────────────────
public class TokenBucketRateLimiter {
    private final Semaphore           tokens;
    private final int                 capacity;
    private final ScheduledExecutorService refiller = Executors.newSingleThreadScheduledExecutor();

    public TokenBucketRateLimiter(int requestsPerSecond) {
        this.capacity = requestsPerSecond;
        this.tokens   = new Semaphore(requestsPerSecond);
        // Refill at the specified rate — every 1s, release up to capacity tokens:
        refiller.scheduleAtFixedRate(this::refill, 1, 1, TimeUnit.SECONDS);
    }

    private void refill() {
        // drainPermits first to avoid over-issuing beyond capacity:
        int current  = tokens.availablePermits();
        int toRelease = capacity - current;
        if (toRelease > 0) tokens.release(toRelease);
    }

    // Block until a token is available:
    public void acquire() throws InterruptedException {
        tokens.acquire();
    }

    // Return false if no token available now:
    public boolean tryAcquire() {
        return tokens.tryAcquire();
    }

    // Return false if no token available within timeout:
    public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException {
        return tokens.tryAcquire(timeout, unit);
    }

    public void shutdown() { refiller.shutdown(); }
}

// Usage:
TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(100);  // 100 req/sec
for (int i = 0; i < 200; i++) {
    final int req = i;
    new Thread(() -> {
        try {
            limiter.acquire();   // first 100 proceed immediately, next 100 wait for refill
            System.out.println("Request " + req + " processed at " + System.currentTimeMillis());
        } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }).start();
}

Binary Semaphore, Fairness, and Semaphore vs Lock

A binary semaphore is a Semaphore with a maximum of 1 permit: new Semaphore(1). It behaves like a mutual exclusion lock in that only one thread can hold the single permit at a time. However, its semantics differ from a lock in two critical ways: it has no owner (any thread can release the permit, not just the one that acquired it), and it is not reentrant (a thread that calls acquire() twice will deadlock on the second acquire, unlike ReentrantLock which allows re-acquisition by the same thread). The non-ownership and non-reentrancy of binary semaphores make them suitable for signaling scenarios where a lock would be semantically wrong. A latch pattern: a Semaphore starts at 0 (no permits); worker threads call acquire() to wait; a signal thread calls release() to let one worker proceed. The signaling thread never acquired the permit, so this cannot be expressed with a lock. Similarly, a thread that acquires a permit and then terminates before releasing it leaves the permit permanently gone — a form of permit "leak" that a lock's owner-semantics would prevent. Fairness in Semaphore works analogously to ReentrantLock: with fair=true, permits are granted in FIFO order among waiting threads; with fair=false (default), a barging thread may bypass waiting threads. Fair semaphores prevent starvation of waiting threads in connection pool scenarios where bursts of new arrivals might otherwise continuously barge past threads that have been waiting. The comparison between Semaphore and other concurrency primitives: use Semaphore when limiting concurrency to N (more than 1); use a Lock (ReentrantLock) when mutual exclusion (N=1) with reentrancy and ownership semantics is needed; use CountDownLatch when waiting for N events to occur (one-shot, cannot be reset); use CyclicBarrier when N threads must all reach a meeting point repeatedly; use BlockingQueue when the producer-consumer pattern with bounded capacity is the primary concern.
Java
// ── Binary semaphore: mutual exclusion without ownership ──────────────
Semaphore mutex = new Semaphore(1);   // 1 permit = binary semaphore

mutex.acquire();
try {
    criticalSection();
} finally {
    mutex.release();   // can be released by ANY thread — not just the acquirer
}

// ── Binary semaphore: signaling between threads ────────────────────────
Semaphore handoff = new Semaphore(0);   // starts at 0: nobody can acquire yet

Thread receiver = new Thread(() -> {
    System.out.println("Receiver: waiting for handoff");
    try { handoff.acquire(); }              // blocks at 0
    catch (InterruptedException e) { Thread.currentThread().interrupt(); return; }
    System.out.println("Receiver: received handoff — proceeding");
});

Thread sender = new Thread(() -> {
    System.out.println("Sender: preparing handoff");
    prepareData();
    handoff.release();   // sender releases a permit it never acquired — non-ownership at work
    System.out.println("Sender: handoff complete");
});

receiver.start();
Thread.sleep(100);
sender.start();
receiver.join(); sender.join();

// ── Non-reentrancy: binary semaphore deadlocks on double acquire ───────
Semaphore nonReentrant = new Semaphore(1);
nonReentrant.acquire();   // succeeds: 1 permit taken, 0 remaining
try {
    // nonReentrant.acquire();   // DEADLOCK: 0 permits — this thread waits forever for itself
    System.out.println("Would deadlock if acquire called again");
} finally {
    nonReentrant.release();
}

// ReentrantLock does NOT deadlock:
ReentrantLock reentrant = new ReentrantLock();
reentrant.lock();
try {
    reentrant.lock();   // reentrant: hold count goes to 2, NO deadlock
    try {
        doNestedWork();
    } finally { reentrant.unlock(); }  // hold count back to 1
} finally { reentrant.unlock(); }      // hold count to 0, released

// ── Fair vs unfair Semaphore: contention under burst ──────────────────
int PERMITS = 2;
Semaphore unfairSem = new Semaphore(PERMITS, false);  // unfair: barge allowed
Semaphore fairSem   = new Semaphore(PERMITS, true);   // fair:   FIFO

// Under sustained burst, unfair may starve some threads indefinitely.
// Fair guarantees every waiting thread eventually gets a permit.

// ── Comparing Semaphore vs other primitives ───────────────────────────
//
// Pattern                     | Tool
// ----------------------------|-----------------------------------------------
// Limit to N concurrent       | Semaphore(N)
// Mutual exclusion (N=1)      | ReentrantLock or synchronized
// Wait for N events, one-shot | CountDownLatch(N)
// N threads meet at barrier   | CyclicBarrier(N) or Phaser
// Producer-consumer, bounded  | BlockingQueue (ArrayBlockingQueue etc.)
// Read-heavy shared state     | ReentrantReadWriteLock or StampedLock
// Inter-thread signaling      | Semaphore(0) or Condition.await/signal
// Publish immutable result    | Future / CompletableFuture

// ── Semaphore as a bounded channel (manual producer-consumer) ──────────
public class BoundedChannel<T> {
    private final Queue<T>    queue;
    private final Semaphore   slots;     // permits = empty slots
    private final Semaphore   items;     // permits = available items

    public BoundedChannel(int capacity) {
        this.queue = new ConcurrentLinkedQueue<>();
        this.slots = new Semaphore(capacity);  // starts full: capacity empty slots
        this.items = new Semaphore(0);         // starts empty: no items yet
    }

    public void put(T item) throws InterruptedException {
        slots.acquire();    // wait for an empty slot
        queue.offer(item);
        items.release();    // signal that an item is available
    }

    public T take() throws InterruptedException {
        items.acquire();    // wait for an item to be available
        T item = queue.poll();
        slots.release();    // signal that a slot is now free
        return item;
    }
}

Related Topics in Multithreading

Process vs Thread
A process is an independent program in execution with its own isolated memory space, file handles, and system resources, managed by the operating system and separated from all other processes by strict boundaries. A thread is a unit of execution that lives inside a process, sharing that process's memory, heap, and resources with every other thread in the same process. Java programs run inside a JVM process; the JVM itself creates and manages threads, and every Java application starts with at least one thread — the main thread — with additional threads created by the JVM for garbage collection, JIT compilation, signal handling, and other runtime tasks. Understanding the distinction between processes and threads is the foundation for all concurrent programming in Java: it determines what is shared and what is isolated, what is fast and what is expensive, what fails independently and what fails together. This entry covers the OS-level and JVM-level model of processes and threads, the memory model that follows from the shared-versus-isolated distinction, the cost model for creation and context switching, failure isolation and its consequences, inter-process and inter-thread communication mechanisms, and the practical decision of when to use multiple processes versus multiple threads.
Thread Basics
A Java thread is an instance of java.lang.Thread that represents an independent path of execution within the JVM process. Every thread has a lifecycle — from creation through runnable, running, blocked, waiting, timed-waiting, and terminated states — and a set of properties including its name, priority, daemon status, thread group, and uncaught exception handler. The Java memory model specifies what visibility guarantees exist between threads and when writes by one thread are guaranteed to be visible to another. Thread scheduling is controlled by the OS scheduler subject to hints from the JVM via thread priority; the JVM does not provide real-time scheduling guarantees. This entry covers the complete thread lifecycle and its state machine, thread properties and how they affect scheduling and JVM shutdown, the happens-before relationship and why it matters for visibility, daemon threads and their relationship to JVM shutdown, thread interruption as a cooperative cancellation mechanism, and the methods on Thread that every Java developer must understand.
Creating Threads
Java provides three primary abstractions for defining the work a thread will execute: the Thread class itself (subclassed to override run()), the Runnable interface (a task with no return value and no checked exception), and the Callable interface (a task with a return value and a declared checked exception). Each represents a different contract between the task and the infrastructure that runs it. Thread subclassing couples the task definition to the execution mechanism and is the oldest and least flexible approach. Runnable decouples the task from the thread, allowing the same Runnable to be submitted to thread pools, scheduled executors, or wrapped in Thread objects. Callable extends that decoupling to include a return value and exception propagation, returning a Future that allows the caller to retrieve the result or handle exceptions asynchronously. Understanding all three — their contracts, their limitations, and when to use each — is the foundation of concurrent programming in Java before reaching for higher-level constructs.
Thread Lifecycle
The Java thread lifecycle is the complete sequence of states a thread passes through from the moment a Thread object is constructed to the moment its execution ends. Java defines six states in the Thread.State enum — NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED — and the JVM transitions threads between these states in response to specific method calls, lock acquisitions, monitor notifications, timeouts, and exceptions. Each state has a precise meaning, a defined set of entry conditions, and a defined set of exit conditions. Understanding the lifecycle in full is prerequisite knowledge for diagnosing deadlocks, thread leaks, performance bottlenecks in thread dumps, and incorrect synchronization — all of which manifest as threads stuck in specific states. This entry covers every state in the lifecycle with its entry and exit conditions, all legal and illegal state transitions, how thread dumps represent each state, the interaction between lifecycle states and interruption, the effect of uncaught exceptions on lifecycle, and how to observe lifecycle transitions programmatically.