Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
202: Semaphores in Java
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
Semaphoreinitialized to 3 permits. - Create a method
downloadImage(String imageUrl)that acquires a permit, simulates a download usingThread.sleep(500), and then releases the permit. - In your
mainmethod, launch 20 threads (using a loop or anExecutorService) to calldownloadImage. - 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.
There are no comments for now.