Mutex vs. Semaphore
Alright, we've tackled deadlocks. Now let's explore two fundamental synchronization primitives that are often confused but serve distinct purposes: Mutex vs. Semaphore. Understanding their differences is key to choosing the right tool for the job.
6. Mutex vs. Semaphore
Concept:
Both Mutexes and Semaphores are mechanisms used to control access to shared resources and prevent race conditions, but they do so in different ways, addressing different types of resource access problems.
-
Mutex (Mutual Exclusion Lock):
- Purpose: Ensures mutual exclusion, meaning only one thread can access a critical section or a shared resource at any given time.
- Analogy: The single-stall bathroom key. There's only one key. If you need to use the bathroom, you must take the key. No one else can enter until you return the key.
- Characteristics:
- It's a binary lock (conceptually 0 or 1, locked or unlocked).
- It has ownership: the thread that acquires the mutex must be the one that releases it. You can't release a mutex that you don't hold.
- Used to protect a single instance of a resource or a critical section of code.
-
Semaphore:
- Purpose: Controls access to a limited number of resources. It's a signaling mechanism that maintains a count.
- Analogy: The parking lot attendant. The parking lot has a fixed number of spaces (e.g., 5). The attendant manages permits (available spaces).
- When a car (thread) arrives, it requests a permit. If permits are available, the attendant gives one, and the car enters (permit count decrements).
- When a car leaves, it returns the permit (permit count increments).
- If no permits are available, arriving cars must wait until one is returned.
- Characteristics:
- It has a count (an integer value, from 0 up to an initial maximum value).
- It typically does not have ownership: any thread can acquire a permit, and any thread can release a permit (though in practice, threads usually release permits they acquired).
- Used to control access to a pool of
N
identical resources or to coordinate activities (producer-consumer). If the count is 1, a semaphore can act like a mutex.
Key Differences Summarized:
Feature | Mutex | Semaphore |
---|---|---|
Purpose | Mutual exclusion (one at a time) | Resource counting / access control for N items |
State/Value | Locked/Unlocked (binary) | Integer count (>= 0) |
Ownership | Yes, owned by the thread that acquired it | No, typically not owned |
Usage | Protect a critical section or single resource | Limit concurrent access to a pool of resources |
Release | Only the acquiring thread can release | Any thread can release a permit |
"Before" (Problematic Code – Uncontrolled Resource Access)
While "Mutex" isn't a direct java.lang.Object
class in Java (the synchronized
keyword and ReentrantLock
act as mutexes), we can demonstrate a scenario where a semaphore would be beneficial.
Scenario: Imagine we have a print server that can handle a maximum of 3 concurrent print jobs. If more than 3 jobs are submitted at once, the additional jobs should wait.
Problem: Without a semaphore (or other control mechanism), if we simply launch threads for print jobs, they might all try to access the "printer" simultaneously, overwhelming it or causing errors if the underlying system truly has a hard limit.
Java Example (Uncontrolled Concurrent Access – no Semaphore):
import java.util.concurrent.*;
public class UncontrolledPrinterAccess {
private static int activePrintJobs = 0; // Simulate number of active print jobs
// This method simulates interacting with a physical printer
public static void printDocument(String documentName) {
System.out.println(Thread.currentThread().getName() + ": Starting print job for " + documentName);
activePrintJobs++; // Increment active job count - UNSAFE without synchronization!
try {
// Simulate printer working
TimeUnit.SECONDS.sleep(ThreadLocalRandom.current().nextInt(1, 4)); // Random print time
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println(Thread.currentThread().getName() + ": Print job interrupted.");
} finally {
activePrintJobs--; // Decrement active job count - UNSAFE without synchronization!
System.out.println(Thread.currentThread().getName() + ": Finished print job for " + documentName + ". Active jobs: " + activePrintJobs);
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("--- Uncontrolled Printer Access ---");
int totalPrintJobs = 10; // Total jobs to submit
// Note: Using a fixed thread pool larger than max_concurrent_jobs to show the problem
ExecutorService executor = Executors.newFixedThreadPool(5); // More threads than allowed concurrent jobs
for (int i = 1; i <= totalPrintJobs; i++) {
final int jobId = i;
executor.submit(() -> printDocument("Document-" + jobId));
}
executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
System.out.println("\nAll print jobs submitted and attempted to complete.");
System.out.println("Final active print jobs (should be 0): " + activePrintJobs);
}
}
Likely Output:
You will see more than 3 "Starting print job…" messages appearing concurrently, indicating that more jobs than the hypothetical limit are trying to run simultaneously. The activePrintJobs
might also show inconsistent values if not properly synchronized (though that's a race condition issue, not just a semaphore one).
--- Uncontrolled Printer Access ---
pool-1-thread-1: Starting print job for Document-1
pool-1-thread-2: Starting print job for Document-2
pool-1-thread-3: Starting print job for Document-3
pool-1-thread-4: Starting print job for Document-4 // 4 jobs running concurrently!
pool-1-thread-5: Starting print job for Document-5 // 5 jobs running concurrently!
... (further output) ...
"After" (Resolved Code – Controlling Resource Access with Semaphore)
Resolution: We will use a java.util.concurrent.Semaphore
to limit the number of threads that can concurrently execute the printDocument
method. We'll initialize the semaphore with 3 permits, allowing only 3 threads to proceed at a time.
Java Example (Controlling Concurrent Access with Semaphore):
import java.util.concurrent.*;
import java.util.concurrent.ThreadLocalRandom; // For random sleep times
public class ControlledPrinterAccessWithSemaphore {
// Define the maximum number of concurrent print jobs
private static final int MAX_CONCURRENT_PRINTERS = 3;
// Create a Semaphore with an initial number of permits
private static final Semaphore printerSemaphore = new Semaphore(MAX_CONCURRENT_PRINTERS);
// This method simulates interacting with a physical printer
public static void printDocument(String documentName) {
try {
System.out.println(Thread.currentThread().getName() + ": Waiting for printer availability (acquire permit)...");
printerSemaphore.acquire(); // Acquire a permit. Blocks if no permits are available.
System.out.println(Thread.currentThread().getName() + ": Acquired permit. Starting print job for " + documentName);
// Simulate printer working
TimeUnit.SECONDS.sleep(ThreadLocalRandom.current().nextInt(1, 4)); // Random print time
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println(Thread.currentThread().getName() + ": Print job interrupted.");
} finally {
printerSemaphore.release(); // Release the permit, allowing another thread to acquire it.
System.out.println(Thread.currentThread().getName() + ": Finished print job for " + documentName + ". Released permit.");
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("--- Controlled Printer Access with Semaphore ---");
int totalPrintJobs = 10;
ExecutorService executor = Executors.newFixedThreadPool(totalPrintJobs); // Enough threads to submit all jobs
for (int i = 1; i <= totalPrintJobs; i++) {
final int jobId = i;
executor.submit(() -> printDocument("Document-" + jobId));
}
executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
System.out.println("\nAll print jobs submitted and completed.");
}
}
Expected Output:
You will now consistently see that no more than 3 threads start their print jobs simultaneously. Threads will acquire a permit, do their work, and release it, allowing other waiting threads to proceed.
--- Controlled Printer Access with Semaphore ---
pool-1-thread-1: Waiting for printer availability (acquire permit)...
pool-1-thread-2: Waiting for printer availability (acquire permit)...
pool-1-thread-3: Waiting for printer availability (acquire permit)...
pool-1-thread-4: Waiting for printer availability (acquire permit)...
pool-1-thread-5: Waiting for printer availability (acquire permit)...
pool-1-thread-1: Acquired permit. Starting print job for Document-1
pool-1-thread-2: Acquired permit. Starting print job for Document-2
pool-1-thread-3: Acquired permit. Starting print job for Document-3
// Notice pool-1-thread-4 and pool-1-thread-5 are waiting...
... (after some time, one of the first 3 finishes) ...
pool-1-thread-2: Finished print job for Document-2. Released permit.
pool-1-thread-4: Acquired permit. Starting print job for Document-4 // Now thread-4 proceeds
... (and so on) ...
Explanation of the Fix:
Semaphore printerSemaphore = new Semaphore(MAX_CONCURRENT_PRINTERS);
: We create aSemaphore
instance and initialize it with3
permits. This means at most 3 threads can acquire a permit concurrently.printerSemaphore.acquire();
: When a thread wants to start a print job, it callsacquire()
.- If a permit is available (count > 0), the count is decremented, and the thread proceeds immediately.
- If no permits are available (count = 0), the thread blocks until another thread
release()
s a permit.
printerSemaphore.release();
: After a thread finishes its print job, it callsrelease()
. This increments the permit count, potentially waking up a waiting thread.
This effectively controls the concurrency level for the "printer resource," ensuring that our hypothetical limit is respected.
Production-Grade Practice Points for Mutex vs. Semaphore:
- Choose the Right Primitive:
- Use
synchronized
(Java's built-in monitor) orReentrantLock
(which we'll discuss later) when you need mutual exclusion – only one thread at a time access. This is your "mutex" equivalent. - Use
Semaphore
when you need to control access to a fixed number of identical resources (e.g., database connection pool, limited worker threads, max concurrent API calls).
- Use
- Fairness in Semaphores:
Semaphore
can be created with a "fairness" policy:new Semaphore(permits, boolean fair)
.- Fair (true): Threads acquire permits in the order they requested them (FIFO). This can reduce throughput due to overhead but prevents starvation.
- Unfair (false – default): Threads acquire permits arbitrarily. This often has higher throughput but can lead to starvation for some threads under heavy contention. Choose based on your requirements.
- Always Release Permits/Locks: Just like with
synchronized
blocks, always ensureacquire()
calls are matched withrelease()
calls, typically within afinally
block to guarantee release even if exceptions occur. Forgetting to release permits or locks leads to resource starvation and potential deadlocks. tryAcquire()
with Semaphore: Similar toReentrantLock
'stryLock()
,Semaphore
hastryAcquire()
which attempts to acquire a permit without blocking, ortryAcquire(long timeout, TimeUnit unit)
which tries to acquire for a specific duration. This is useful for non-blocking or time-bound resource requests.- Beyond
synchronized
andSemaphore
: While fundamental, for more complex coordination patterns (like producer-consumer with buffers),java.util.concurrent.BlockingQueue
often provides an even higher-level, more robust, and easier-to-use solution than directwait
/notify
or semaphores. We'll look at these in "High-Level Concurrency Utilities".
This covers the essential difference and usage of Mutex-like behavior (synchronized
/ ReentrantLock
) and Semaphore
.
Next, we'll quickly clarify Mutex vs. Monitor and Semaphore vs. Monitor as they build on these concepts. Ready for the next one?