In multi-threaded applications, managing access to shared resources is crucial for maintaining system stability and performance. While standard mutual exclusion locks (Mutexes or synchronized blocks) enforce a strict "one thread at a time" policy, some resources can handle multiple concurrent operations up to a specific limit.

To solve this, Java provides the Semaphore class inside the java.util.concurrent package. A semaphore maintains a set of virtual permits. Threads can access the guarded resource only if they can successfully acquire a permit. By dynamically managing these permits, semaphores allow developers to implement robust concurrency throttling and rate-limiting. In this guide, we will analyze Java Semaphores and build a thread-throttled ATM simulator.

Visualizing Java Semaphore permits
Real-World Analogy: The ATM Vestibule and the Key Guard

To visualize semaphore access control, imagine a bank vestibule containing exactly 2 ATM machines:

  • Alice and Bob (Acquiring Permits): Alice arrives first. The guard hands her a key, and she enters the booth (1 key remaining). Next, Bob arrives. The guard hands him the second key, and he enters (0 keys remaining).
  • Charlie (Blocking Queue): Charlie arrives third. Since no keys remain, the guard makes him wait in a queue outside the door. Charlie's execution blocks.
  • Alice Leaves (Releasing Permits): Once Alice finishes her transaction and steps out, she returns the key to the guard (releasing a permit). The guard immediately hands the key to Charlie, who can now enter.
In this system, the keys represent semaphore permits, the guard coordinates thread access, and the vestibule represents the critical section.

Semaphore Types & Mechanics

Java supports two main types of semaphores:

  1. Binary Semaphore: Initialized with exactly 1 permit. It behaves like a Mutex, restricting access to a single thread at a time.
  2. Counting Semaphore: Initialized with $N$ permits (where $N > 1$). It allows up to $N$ threads to run concurrently.
When a thread calls acquire(), it blocks until a permit is available. Conversely, calling release() increments the permit count, notifying blocked threads. Crucially, you should always release permits inside a finally block; otherwise, an unexpected runtime exception inside the critical section will cause a permit leak, permanently blocking other waiting threads. Additionally, Java's Semaphore constructor accepts an optional fair boolean flag. Setting new Semaphore(2, true) guarantees that waiting threads acquire permits in a strict First-In-First-Out (FIFO) order, preventing thread starvation, though it introduces a slight performance overhead.

Java Implementation Code

Below is the complete Java implementation of our ATM vestibule simulator using a counting semaphore to throttle threads:

package io.practise.threadsExample;
 
import java.util.concurrent.Semaphore;
 
public class SemaphoreExample {
  // Only 2 threads can access the resource simultaneously
  static Semaphore semaphore = new Semaphore(2);
 
  public static void main(String[] args) {
      new MyAtmThread("Alice").start();
      new MyAtmThread("Bob").start();
      new MyAtmThread("Charlie").start();
  }
 
  static class MyAtmThread extends Thread {
      public MyAtmThread(String name) {
          super(name);
      }
 
      @Override
      public void run() {
          try {
              System.out.println(getName() + " is waiting to enter the ATM booth...");
              // Acquire a permit
              semaphore.acquire();
              System.out.println(getName() + " entered the booth and is doing transactions.");
              Thread.sleep(2000); // Simulate transaction processing
              System.out.println(getName() + " is leaving the ATM booth.");
          } catch (InterruptedException e) {
              e.printStackTrace();
          } finally {
              // Always release permit in finally block to prevent thread deadlocks
              semaphore.release();
          }
      }
  }
}

Conclusion & Best Practices

Java Semaphores are highly useful for rate-limiting outward calls to third-party APIs, throttling connections to database pools, or shielding expensive physical server resources from sudden concurrent traffic spikes. By sizing your permits appropriately, you prevent downstream resource starvation and maintain consistent system performance.