Semaphores in Java

KNOWLEDGE GAP - LEARN MORE, IMPLEMENT THIS

Semaphores:

  1. Semaphores in Java https://www.baeldung.com/java-semaphore
  2. Class Semaphore https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Semaphore.html
  3. What is a Java semaphore? https://redisson.pro/glossary/java-semaphore.html
  4. Semaphore in Java https://www.geeksforgeeks.org/java/semaphore-in-java/

In Java, a Semaphore is a concurrency utility found in the java.util.concurrent package. It controls access to a shared resource by maintaining a count of available “permits.” Threads must acquire a permit before accessing the resource and release it afterward.

Here’s how it works:

  1. Initialization: A Semaphore is created with an initial number of permits. This number represents the maximum concurrent access allowed to the resource.
    Semaphore semaphore = new Semaphore(3); // Allows up to 3 threads to access concurrently
    
  1. Acquiring Permits: When a thread wants to access the shared resource, it calls the acquire() method.

    1. If permits are available, the thread acquires one, and the permit count decreases.
    2. If no permits are available, the thread blocks until a permit is released by another thread.
    try {
            semaphore.acquire(); // Acquire a permit
            // Critical section: Access the shared resource
    } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
    } finally {
            // Ensure permit is released even if an exception occurs
            semaphore.release();
    }
    
  2. Releasing Permits: After a thread has finished using the shared resource, it calls the release() method to return a permit to the semaphore. This increases the permit count, potentially allowing a waiting thread to acquire a permit and proceed.

    semaphore.release(); // Release the permit

Types of Semaphores:

  1. Counting Semaphore: Allows a specified number of threads to access a resource concurrently (e.g., the Semaphore(3) example above).
  2. Binary Semaphore (Mutex): A special case of a counting semaphore initialized with a single permit (Semaphore(1)). It acts as a mutual exclusion lock, ensuring only one thread can access the resource at a time. Unlike Lock implementations, a binary semaphore can be released by a thread other than the one that acquired it.

Common Uses:

  1. Limiting resource usage: Controlling the number of concurrent connections to a database, file handles, or network sockets.
  2. Controlling access to a critical section: Ensuring only a certain number of threads can execute a specific block of code simultaneously.
  3. Signaling between threads: One thread can release a permit to signal another thread that a condition has been met, allowing the waiting thread to proceed.

Controlling Concurrency with Precision

Source: https://medium.com/javarevisited/semaphore-in-java-6824fe663975

Introduction

Controlling access to shared resources is a fundamental challenge in the realm of concurrent programming. Java provides several concurrency utilities to manage such access effectively, including Semaphore. Semaphores are powerful constructs that can be used to restrict the number of threads accessing a resource concurrently. This article delves into the workings of Semaphore, exploring its usage, advantages, disadvantages, and practical implementation in Java. By the end of this comprehensive guide, you will have a thorough understanding of how to use Semaphore to manage concurrency in your Java applications.

What is a Semaphore?

A Semaphore is a concurrency utility that controls access to a shared resource through the use of permits. It maintains a set of permits, and threads can acquire or release these permits to enter or exit a critical section. The Semaphore class is part of the java.util.concurrent package, which provides high-level concurrency constructs introduced in Java 5.

Key Features of Semaphore

  1. Permits: The number of permits determines how many threads can access the resource simultaneously.
  2. Acquiring Permits: Threads can acquire permits using the acquire() method. If no permits are available, the thread will block until one is released.
  3. Releasing Permits: Threads can release permits using the release() method, making them available for other threads.
  4. Fairness: Semaphores can be configured to follow a first-in-first-out (FIFO) ordering for acquiring permits, ensuring fairness.

When to Use Semaphore?

Semaphores are particularly useful in scenarios where you need to limit the number of concurrent accesses to a shared resource. Here are some common use cases:

  1. Resource Pools: Managing a fixed number of resources, such as database connections or thread pools.
  2. Rate Limiting: Limiting the rate of requests or tasks being processed concurrently.
  3. Critical Sections: Protecting critical sections of code that should only be accessed by a specific number of threads simultaneously.
  4. Coordination: Synchronizing multiple threads to perform certain tasks with controlled concurrency.

How to Use Semaphore?

Using Semaphore involves a few key steps:

  1. Initialize the Semaphore: Create an instance of Semaphore with a specified number of permits.
  2. Acquire Permits: Use the acquire() method to request permits before entering a critical section.
  3. Release Permits: Use the release() method to return permits after exiting the critical section.

Example: Basic Usage of Semaphore

Let’s look at a simple example to illustrate the basic usage of Semaphore.

import java.util.concurrent.Semaphore;

public class SemaphoreExample {

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);  // Three permits

        for (int i = 0; i < 10; i++) {
            new Thread(new Worker(semaphore)).start();
        }
    }
}

class Worker implements Runnable {
    private final Semaphore semaphore;

    Worker(Semaphore semaphore) {
        this.semaphore = semaphore;
    }

    @Override
    public void run() {
        try {
            semaphore.acquire();  // Acquire a permit
            System.out.println(Thread.currentThread().getName() + " is working.");
            Thread.sleep((long) (Math.random() * 1000));  // Simulate work
            System.out.println(Thread.currentThread().getName() + " has finished.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release();  // Release the permit
        }
    }
}

In this example, ten worker threads are created, but only three can access the critical section simultaneously due to the semaphore’s three permits.

Advanced Usage of Semaphore

Fairness

Semaphores can be configured to use a fairness policy. A fair semaphore ensures that threads acquire permits in the order they requested them, preventing thread starvation.

Semaphore fairSemaphore = new Semaphore(3, true);  // Three permits, fair ordering

Acquiring Multiple Permits

A semaphore can allow threads to acquire multiple permits at once, which is useful when a thread requires more than one unit of the resource.

semaphore.acquire(2);  // Acquire two permits

Non-Blocking Acquisition

Threads can attempt to acquire permits without blocking using the tryAcquire() method, which returns true if permits are available and false otherwise.

if (semaphore.tryAcquire()) {
    // Perform the task
} else {
    // Handle the case where permits are not available
}

Acquiring with Timeout

Threads can attempt to acquire permits with a timeout, which is useful to avoid indefinite blocking.

if (semaphore.tryAcquire(1, TimeUnit.SECONDS)) {
    // Perform the task
} else {
    // Handle the timeout case
}

Real-World Scenarios

Example 1: Database Connection Pool

Consider a scenario where you need to manage a fixed number of database connections. Semaphores can be used to limit the number of threads accessing the database simultaneously.

import java.util.concurrent.Semaphore;

public class DatabaseConnectionPool {

    private static final int MAX_CONNECTIONS = 5;
    private final Semaphore semaphore = new Semaphore(MAX_CONNECTIONS, true);

    public void connect() {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName() + " acquired a database connection.");
            // Simulate database operations
            Thread.sleep((long) (Math.random() * 1000));
            System.out.println(Thread.currentThread().getName() + " released a database connection.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release();
        }
    }

    public static void main(String[] args) {
        DatabaseConnectionPool pool = new DatabaseConnectionPool();

        for (int i = 0; i < 10; i++) {
            new Thread(pool::connect).start();
        }
    }
}

In this example, the semaphore ensures that no more than five threads can access the database simultaneously.

Example 2: Rate Limiting

Semaphores can be used to implement rate limiting, controlling the number of tasks processed concurrently.

import java.util.concurrent.Semaphore;

public class RateLimiter {

    private final Semaphore semaphore;

    public RateLimiter(int maxConcurrentRequests) {
        semaphore = new Semaphore(maxConcurrentRequests, true);
    }

    public void handleRequest() {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName() + " is handling a request.");
            // Simulate request processing
            Thread.sleep((long) (Math.random() * 1000));
            System.out.println(Thread.currentThread().getName() + " has finished handling the request.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release();
        }
    }

    public static void main(String[] args) {
        RateLimiter rateLimiter = new RateLimiter(3);

        for (int i = 0; i < 10; i++) {
            new Thread(rateLimiter::handleRequest).start();
        }
    }
}

In this example, the semaphore limits the number of concurrent request handling threads to three.

Advantages of Semaphore

  1. Concurrency Control: Semaphores provide precise control over the number of threads accessing a shared resource.
  2. Flexibility: Semaphores can be used in a variety of scenarios, from simple mutual exclusion to complex synchronization tasks.
  3. Fairness: Fair semaphores ensure that threads are granted permits in the order they requested them, preventing starvation.

Disadvantages of Semaphore

  1. Complexity: Using semaphores can add complexity to the code, making it harder to read and maintain.
  2. Potential for Deadlock: Improper usage can lead to deadlocks, especially if permits are not released correctly.
  3. Performance Overhead: Acquiring and releasing permits can introduce performance overhead, particularly in high-contention scenarios.

Implementing a Semaphore in Spring Boot

Spring Boot provides excellent support for concurrency through its integration with the Java concurrency utilities. Let’s look at how to implement a semaphore in a Spring Boot application.

Example: Limiting Concurrent Requests to an Endpoint

Suppose we want to limit the number of concurrent requests to a specific endpoint in a Spring Boot application. We can use a semaphore to achieve this.

Create a Controller

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.Semaphore;

@RestController
public class RequestController {

    private final Semaphore semaphore = new Semaphore(3);

    @GetMapping("/limited")
    public String handleRequest() {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName() + " is handling a request.");
            // Simulate request processing
            Thread.sleep((long) (Math.random() * 1000));
            return "Request handled by " + Thread.currentThread().getName();
        } catch (InterruptedException e) {
            e.printStackTrace();
            return "Error handling request";
        } finally {
            semaphore.release();
        }
    }
}
  1. Run the Application

Start the Spring Boot application and access the /limited endpoint from multiple clients. The semaphore will ensure that no more than three requests are processed concurrently.

Challenges and Considerations

While semaphores are powerful tools for managing concurrency, they come with their own set of challenges and considerations:

  1. Deadlocks: Improper usage can lead to deadlocks if permits are not released correctly or if threads hold permits indefinitely.
  2. Resource Starvation: Without proper fairness, threads can be starved of permits, leading to indefinite blocking.
  3. Debugging Difficulty: Debugging issues related to semaphores can be challenging, especially in complex systems with many concurrent threads.
  4. Performance Impact: High contention for permits can lead to performance bottlenecks, especially if the critical section is lengthy.

Conclusion

Semaphores are versatile and powerful concurrency tools in Java that provide fine-grained control over the number of threads accessing a shared resource. By understanding the key features, advantages, and potential pitfalls of semaphores, developers can effectively use them to manage concurrency in various scenarios, from resource pooling to rate limiting.

In this comprehensive guide, we explored the basics of semaphores, delved into advanced usage scenarios, and provided practical examples to illustrate their application in real-world situations. Additionally, we discussed how to implement semaphores in a Spring Boot application and highlighted the challenges and considerations when working with semaphores.

By mastering the concepts covered in this article, you will be well-equipped to leverage semaphores to build robust and efficient concurrent applications in Java.


Links to this note