Semaphore is an advanced synchronization mechanism used to control access to a shared resource by multiple threads. It can maintain a set of permits, restricting how many threads can concurrently access a critical section or shared resource. If the permits are exhausted, additional threads will block until permits are released.
How to Use Semaphore for Resource Control
Here are the key steps for using a Semaphore:
1. Initialization
- Permits: When creating Semaphore, specify the number of permits. This determines the maximum number of threads that can access the resource simultaneously.
- Fairness: Optionally, you can specify a fairness policy (
truefor FIFO access to permits,falsefor default behavior).Example:
Semaphore semaphore = new Semaphore(2, true); // 2 permits, FIFO fairness
2. Acquiring Permits
Threads must acquire permits before accessing the shared resource. The acquire() method blocks the thread if no permits are available.
- Interruptible Acquire:
acquire()blocks until a permit becomes available.semaphore.acquire(); - Immediate Acquire:
tryAcquire()attempts to acquire and doesn’t block. Returnstrueif successful,falseotherwise.if (semaphore.tryAcquire()) { // Acquired permit } - Timed Acquire:
tryAcquire(timeout, TimeUnit)waits for a permit for a specified amount of time before giving up.if (semaphore.tryAcquire(2, TimeUnit.SECONDS)) { // Acquired permit }
3. Using the Shared Resource
After acquiring a permit, the thread performs its task within the critical section or accesses the shared resource.
Example:
// Critical section
System.out.println(Thread.currentThread().getName() + " is using the resource");
4. Releasing Permits
After completing the task, the thread should release the permit it acquired. This allows other threads to proceed.
- Use
release()to give up the permit:semaphore.release();
If a thread fails to release its permit due to an exception or oversight, other threads might starve waiting for permits.
Example of Semaphore in Practice
Here’s a practical example:
package org.kodejava.util.concurrent;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
// Semaphore initialized with 2 permits (only 2 threads can access simultaneously).
private static final Semaphore semaphore = new Semaphore(2);
public static void main(String[] args) {
// Create a thread pool with 5 threads
try (ExecutorService executorService = Executors.newFixedThreadPool(5)) {
// Let each thread try to acquire a permit and access a shared resource
for (int i = 1; i <= 5; i++) {
final int threadId = i;
executorService.submit(() -> {
try {
System.out.println("Thread " + threadId + " is trying to acquire a permit.");
semaphore.acquire();
System.out.println("Thread " + threadId + " has acquired a permit.");
Thread.sleep(2000); // Simulate using the shared resource
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
System.out.println("Thread " + threadId + " is releasing the permit.");
semaphore.release();
}
});
}
}
}
}
Key Concepts of Semaphores
- Permits:
- Semaphore tracks the number of remaining permits.
- Initial permits are specified at the time of creation.
- Blocking vs Non-blocking Acquire:
- Threads may block (
acquire()), attempt immediate access (tryAcquire()), or timeout (tryAcquire(timeout)).
- Threads may block (
- Fairness:
- Semaphore fairness ensures FIFO granting of permits if fairness is enabled.
- Common Usage Scenarios:
- Throttling: Limit the number of threads accessing resources like database connections or file IO simultaneously.
- Rate Limiting: Control the frequency of tasks or API calls.
- Thread-Safe: The semaphore internally ensures thread-safety using synchronization primitives.
By using these steps, you can effectively use semaphore to control access to a shared resource, ensuring both mutual exclusion and efficient resource utilization.
