1. The “Why”
A standard Mutex or synchronized block only allows one thread at a time. A Semaphore, however, maintains a set of permits.
- It is used to limit the number of concurrent threads accessing a specific resource (e.g., only 5 database connections allowed).
- In a Producer-Consumer scenario, we use it to signal when “Space is Available” (for the producer) and when “Items are Available” (for the consumer).
2. Comparison: Mutex vs. Semaphore
| Feature | Mutex (or Binary Semaphore) | Counting Semaphore |
|---|---|---|
| Permit Count | Exactly 1. | $N$ (User-defined). |
| Ownership | Only the thread that locked it can unlock it. | No ownership. Any thread can release a permit. |
| Primary Use | Protecting a Critical Section (Safety). | Signalling and Resource Throttling (Coordination). |
| Analogy | A bathroom with one key. | A parking lot with $N$ spots. |
3. The “Golden” Snippet: Semaphore-Based Producer-Consumer
This implementation uses two semaphores to coordinate a shared buffer. One tracks “Full” slots and the other tracks “Empty” slots.
import java.util.concurrent.Semaphore;
import java.util.LinkedList;
import java.util.Queue;
public class SharedBuffer {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity = 5;
// Initially: 5 empty slots, 0 full slots
private final Semaphore emptySlots = new Semaphore(capacity);
private final Semaphore fullSlots = new Semaphore(0);
private final Semaphore mutex = new Semaphore(1); // To protect the queue
public void produce(int item) throws InterruptedException {
emptySlots.acquire(); // Wait for an empty slot
mutex.acquire(); // Lock the queue
queue.add(item);
System.out.println("Produced: " + item);
mutex.release(); // Unlock queue
fullSlots.release(); // Signal that an item is available
}
public void consume() throws InterruptedException {
fullSlots.acquire(); // Wait for an item
mutex.acquire(); // Lock the queue
int item = queue.poll();
System.out.println("Consumed: " + item);
mutex.release(); // Unlock queue
emptySlots.release(); // Signal that a slot is now empty
}
}
Code Explanation:
acquire(): If the permit count is 0, the thread blocks. If > 0, it decrements the count and proceeds.release(): Increments the permit count and wakes up any waiting threads.- Coordination Logic:
- The Producer “consumes” an empty slot and “produces” a full slot.
- The Consumer “consumes” a full slot and “produces” an empty slot.
- The Mutex Semaphore: Since a
LinkedListis not thread-safe, we use a third semaphore with 1 permit as a lock to ensure only one thread modifies the actual data structure at a time.
Example Output:
Produced: 1
Produced: 2
Consumed: 1
Produced: 3
... (Buffer fills up) ...
Produced: 5
[Producer Blocks - Waiting for emptySlots]
Consumed: 2
[Producer Wakes up and adds next item]
4. The Gotchas
- Deadlock via Order: If you call
mutex.acquire()beforeemptySlots.acquire(), you will cause a deadlock. The producer will hold the lock while waiting for a slot that will never open because the consumer is blocked trying to get the lock!- Rule: Always acquire the “Signalling” semaphore before the “Mutex” semaphore.
- Release without Acquire: Unlike locks, you can call
release()on a semaphore even if you never calledacquire(). This can accidentally increase the number of permits beyond your intended capacity if your logic is buggy. - Fairness: Like
ReentrantLock, Semaphores can be initialized with a “fair” parameter (new Semaphore(n, true)) to ensure the longest-waiting thread gets the permit first, though this reduces throughput.