To control access to shared resources in a multithreaded environment, a Semaphore is frequently used, which is part of the java.util.concurrent
package. A semaphore manages a set number of permits that control how many threads can access a shared resource simultaneously. Threads acquire permits before accessing the resource and release the permits after they are done, ensuring controlled and synchronized access.
Here’s how you can use a semaphore to control access to resources:
1. Key Points About Semaphore:
- Permits: The semaphore holds a set number of permits, which represent the number of threads that can access the resource concurrently.
- Acquire/Release:
- A thread must acquire a permit using the
acquire()
method to access the resource. - It must release the permit using
release()
after finishing its access to the resource.
- A thread must acquire a permit using the
- Blocking Behavior: If no permits are available, the acquiring thread will block until a permit is released by another thread.
- Fairness: You can construct a semaphore in a fair mode to ensure that waiting threads acquire permits in the order they requested them.
2. Example: Semaphore with Limited Access to Resources
Here is a simple example where a semaphore is used to control access to a shared resource (e.g., a connection pool or a printer):
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
ExecutorService executorService = Executors.newFixedThreadPool(5);
// Simulate 5 threads trying to access the shared resource
for (int i = 1; i <= 5; i++) {
final int threadId = i;
executorService.submit(() -> {
try {
// Try to acquire a permit
System.out.println("Thread " + threadId + " is trying to acquire a permit.");
semaphore.acquire(); // Blocks if no permit is available
// Access the shared resource
System.out.println("Thread " + threadId + " has acquired a permit.");
Thread.sleep(2000); // Simulate the resource usage
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// Release the permit after use
System.out.println("Thread " + threadId + " is releasing the permit.");
semaphore.release();
}
});
}
executorService.shutdown();
}
}
3. How It Works:
- Initialization: The semaphore is initialized with a number of permits (
new Semaphore(2)
), allowing only 2 threads to access the resource concurrently. - Acquire: A thread attempts to access the resource by calling
semaphore.acquire()
. If permits are unavailable, the thread is blocked until a permit is released by another thread. - Critical Section: Once the permit is acquired, it enters the critical section and uses the resource.
- Release: After the thread is done using the resource, it calls
semaphore.release()
to return a permit, allowing other threads to acquire it.
4. Output Example:
When you run the example above, you might see an output like this, showing how only 2 threads can access the resource simultaneously:
Thread 1 is trying to acquire a permit.
Thread 1 has acquired a permit.
Thread 2 is trying to acquire a permit.
Thread 2 has acquired a permit.
Thread 3 is trying to acquire a permit.
Thread 1 is releasing the permit.
Thread 3 has acquired a permit.
Thread 4 is trying to acquire a permit.
Thread 2 is releasing the permit.
Thread 4 has acquired a permit.
Thread 5 is trying to acquire a permit.
Thread 3 is releasing the permit.
Thread 5 has acquired a permit.
Thread 4 is releasing the permit.
Thread 5 is releasing the permit.
Here, only 2 threads are allowed to acquire permits at a time, while others are blocked until permits are released.
5. Fair Ordering:
If you want the semaphore to provide fairness (FIFO order), you can use the constructor:
Semaphore semaphore = new Semaphore(2, true);
The second argument (true
) enables fair ordering, making sure the threads acquire permits in the order they requested them.
6. Use Cases:
- Database Connection Pools: Managing the number of simultaneous connections to a database.
- Printers: Limiting how many jobs can access a shared printer.
- Rate Limiting: Throttling the number of threads processing tasks in high-volume systems.