Avoiding Common Pitfalls When Using Java Semaphore

A Semaphore can be used to restrict concurrent access to a shared resource. However, subtle errors in its usage can lead to unexpected behavior, such as threads becoming blocked or the semaphore's permit count becoming incorrect.

Consider a scenario where three threads attempt to acquire permits from a Semaphore initialized with a count of 2. A typical mistake is to incorrectly check the number of available permits.

import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

public class ConcurrentAccessDemo {
    public static void main(String[] args) {
        int maxConcurrent = 2;
        Semaphore gatekeeper = new Semaphore(maxConcurrent, true);

        Thread workerAlpha = new Thread(new ResourceUser(1, "Alpha", gatekeeper), "Worker-Alpha");
        Thread workerBeta = new Thread(new ResourceUser(2, "Beta", gatekeeper), "Worker-Beta");
        Thread workerGamma = new Thread(new ResourceUser(1, "Gamma", gatekeeper), "Worker-Gamma");

        workerAlpha.start();
        workerBeta.start();
        workerGamma.start();
    }
}

class ResourceUser implements Runnable {
    private final int requiredSlots;
    private final String taskName;
    private final Semaphore controller;

    public ResourceUser(int slots, String name, Semaphore sem) {
        this.requiredSlots = slots;
        this.taskName = name;
        this.controller = sem;
    }

    @Override
    public void run() {
        try {
            // Incorrect usage: drainPermits() consumes all permits
            System.out.println("Available permits before: " + controller.drainPermits());
            controller.acquire(requiredSlots);
            System.out.println(Thread.currentThread().getName() + " acquired " + requiredSlots + " permit(s) for " + taskName);
            int duration = ThreadLocalRandom.current().nextInt(1, 5);
            TimeUnit.SECONDS.sleep(duration);
            System.out.println(Thread.currentThread().getName() + " finished after " + duration + " seconds");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            controller.release(requiredSlots);
            System.out.println("Permits after release: " + controller.availablePermits());
        }
    }
}

Running this code might result in only the first thread executing, while others remain blocked. The issue lies in the use of drainPermits() on line 34, which acquires and returns all remaining permits, potentially leaving none for subsequent threads. This method should be replaced with availablePermits(), which only inspects but does not modify the permit count.

Key Method Differences

  • availablePermits(): Returns the current number of permits available without affecting the Semaphore's state.
  • drainPermits(): Atomically acquires and returns all permits currently available, resetting the count to zero.
  • acquire(int permits): Requests the specified number of permits, blocking if insufficient are available. It can throw InterruptedException.
  • release(int permits): Returns the specified number of permits to the Semaphore, potentially making them available for other waiting threads.

The release method documentation states that the thread calling release is not required to have previously called acquire. This is a significant source of errors. A common pattern places release in a finally block to ensure cleanup. However, if an InterruptedException occurs before acquire succeeds, the finally block will still execute release, incorrectly increasing the permit count.

// Example of the problematic pattern
public void run() {
    try {
        controller.acquire(requiredSlots);
        // ... use resource
    } catch (InterruptedException e) {
        // Thread was interrupted before acquiring!
        Thread.currentThread().interrupt();
    } finally {
        // This will execute even if acquire() failed, causing a bug.
        controller.release(requiredSlots);
    }
}

To fix this, ensure release is only called if the permit was successfully acquired.

@Override
public void run() {
    boolean acquired = false;
    try {
        controller.acquire(requiredSlots);
        acquired = true;
        System.out.println(Thread.currentThread().getName() + " acquired permits.");
        // ... use resource
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        if (acquired) {
            controller.release(requiredSlots);
        }
    }
}

Enhanced Semaphore Wrapper

A more robust solution is to create a wrapper that tracks which threads have acquired permits and only allows them to release.

import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Semaphore;

public class TrackedSemaphore {
    private final Semaphore internalSemaphore;
    private final ConcurrentLinkedQueue<Thread> permitHolders = new ConcurrentLinkedQueue<>();

    public TrackedSemaphore(int permits) {
        this.internalSemaphore = new Semaphore(permits);
    }

    public void acquire(int permits) throws InterruptedException {
        internalSemaphore.acquire(permits);
        permitHolders.add(Thread.currentThread());
    }

    public void release(int permits) {
        if (!permitHolders.contains(Thread.currentThread())) {
            throw new IllegalStateException("Current thread did not acquire permits from this semaphore.");
        }
        permitHolders.remove(Thread.currentThread());
        internalSemaphore.release(permits);
    }

    public int availablePermits() {
        return internalSemaphore.availablePermits();
    }
}

This wrapper adds a check in the release method, preventing non-owning threads from incorrectly releasing permits.

Tags: java Concurrency Semaphore multithreading Synchronization

Posted on Mon, 11 May 2026 00:20:40 +0000 by Cory94bailly