Skip to Content
Course content

202: Semaphores in Java

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was lead on a project for a fintech client that relied on a legacy mainframe API. This API was ancient and fragile; the documentation explicitly stated that if more than five concurrent requests hit the endpoint, the mainframe would simply drop all connections and trigger a manual reset by their sysadmins. Now, we had a modern Java backend handling thousands of users. I watched a junior developer try to solve this using a synchronized block around the API client. It "worked" in the sense that it stopped the crashes, but it killed our performance because it limited us to exactly one request at a time, even though the mainframe could handle five.

He was thinking in terms of mutual exclusion—the idea that only one thread can enter a critical section. But we didn't need a lock; we needed a throttle. That's where the Semaphore comes in.

Controlling Access with Permits

Think of a Semaphore not as a lock, but as a bucket of permits. When you initialize a Semaphore, you tell it exactly how many permits it holds. If you set it to 5, the first five threads that call acquire() will grab a permit and proceed immediately. The sixth thread, however, will be blocked. It will sit there and wait until one of the first five threads calls release(), putting a permit back into the bucket.

Here is how we solved that mainframe issue. Instead of locking the whole method, we used a semaphore to manage the specific number of allowed concurrent calls:

import java.util.concurrent.Semaphore;

public class MainframeClient {
    // We allow exactly 5 concurrent permits
    private final Semaphore throttle = new Semaphore(5);

    public void callLegacyApi() {
        try {
            // This will block if 5 threads are already inside
            throttle.acquire(); 
            
            System.out.println(Thread.currentThread().getName() + " is accessing the mainframe...");
            // Simulate the network call
            Thread.sleep(1000); 
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            // CRITICAL: Always release in a finally block
            throttle.release();
            System.out.println(Thread.currentThread().getName() + " released the permit.");
        }
    }
}

The Peril of the Leaked Permit

I cannot stress this enough: always wrap your release() call in a finally block. In the early days of my career, I once wrote a semaphore-based rate limiter where a specific runtime exception was skipping the release call. Over the course of a week, the "bucket" of permits slowly emptied. Eventually, the system hit a state where zero permits were available, and every single thread in the application just stopped, waiting for a permit that would never be returned. It was a "silent" deadlock that took hours to debug because the CPU usage was 0%—the app wasn't crashing; it was just waiting.

You should also be aware of "fairness." By default, semaphores don't guarantee the order in which threads acquire permits. If you have ten threads waiting, the JVM might let the tenth thread jump the line. If your use case requires a strict First-In-First-Out (FIFO) order, you can pass true into the constructor: new Semaphore(5, true). Just be warned that fairness comes with a slight performance hit because of the extra bookkeeping the JVM has to do.

Binary Semaphores vs. Mutexes

You'll often hear people talk about "Binary Semaphores." This is simply a semaphore initialized with a count of 1. You might wonder why you'd use that instead of a ReentrantLock. The biggest difference is ownership. A Lock is owned by the thread that locked it; only that specific thread can unlock it. A Semaphore has no concept of ownership. Thread A can call acquire(), and Thread B can call release().

While that sounds dangerous, it's incredibly useful for signaling between threads. For example, you can have one thread waiting on a semaphore with 0 permits, and another thread that "signals" it to start by releasing a permit. It's a lightweight way to coordinate the hand-off of work between different parts of your system.




📋 Practical Task

Exercise: Building a Throttled Image Downloader

You are building a tool that downloads 20 high-resolution images from a remote server. However, the server has a strict policy: if you attempt more than 3 concurrent downloads from the same IP, it will flag you as a bot and block your access.

Your Task: Implement a class ImageDownloadManager that uses a Semaphore to ensure that no more than 3 threads are downloading images simultaneously.

Requirements:

  • Create a Semaphore initialized to 3 permits.
  • Create a method downloadImage(String imageUrl) that acquires a permit, simulates a download using Thread.sleep(500), and then releases the permit.
  • In your main method, launch 20 threads (using a loop or an ExecutorService) to call downloadImage.
  • Add print statements to show when a thread is "Waiting for permit", "Downloading", and "Finished".
  • Ensure that the permits are released even if a simulated exception occurs during the download.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.