Multithreading can be unpredictable and complex. At the same time, it’s undoubtedly one of the most critical concepts every Java developer needs to master. But who says that diving into such an intricate topic can’t be enjoyable? In this article, we’ll explore Java Semaphore through a playful, real-world analogy—proving that even learning concurrency can be fun!
The Tale of Hungry Customers & Limited Tables
So, let’s start with a hypothetical scenario. Let me introduce The Technical Musings Cafe – a Java-powered cafe where curious minds and tech enthusiasts can dine and code simultaneously. But what started as a small venture quickly turned into absolute chaos.
The cafe’s popularity skyrocketed, with hungry customers lining up to get a seat at this tech-inspired eatery. It’s quite exciting! However, there is just one problem – the number of tables at the cafe is limited. Therefore, we need to find a way to fairly allocate the tables without causing chaos among hungry techies.
Something must be done. A mechanism must be put in place to manage the chaos… but what?
Welcome Semaphores: The Cafe’s Secret Tool for Chaos Management
We already know how to solve such problems in the real world – we need a manager who will only allow as many customers into the cafe as there are available tables. Therefore, to address the same challenge programmatically, we have just the right solution – Semaphore. Before we solve our Cafe’s problem, let’s first learn more about Semaphore.
What Is a Semaphore?
In simple terms, Semaphore is a synchronization tool that is used to restrict the number of threads that can access a given shared resource or critical section. Essentially, it can be as simple as an abstract data type that maintains a count, known as permit, of threads that are allowed to access a shared resource.
How Semaphore Works?
First, a thread that requires access to the shared resource acquires the Semaphore. Consequently, the semaphore reduces the number of available permits by 1. Then, when the thread is done, it releases the semaphore, thereby incrementing the available permits by 1.
Furthermore, when there are no permits available with the semaphore, the threads wait till a permit becomes available.
Types of Semaphore
A semaphore with multiple available permits is referred to as a Counting Semaphore.
Conversely, a Binary Semaphore is a semaphore with only one available permit. In this scenario, the semaphore functions as a monitor or lock. Because it permits only one thread to access the resource or critical section.
Java Code: Managing the Cafe Table Access Using Semaphores
Returning to the issue of our cafe, a counting Semaphore can be employed with the permit count set to the number of tables available. The Semaphore will ensure that customers are allocated tables as they become available. Furthermore, the advantage is that implementing a Semaphore is straightforward. So, let’s now gear up, code, and make the Technical Musings Cafe run smoothly.
To start with, we’ll create two classes – CafeHungerManager and TheTechnicalMusingsCafe. The complete code for this example is available here.
Then, we’ll make the CafeHungerManager class an entry point with the main method. This class initializes the TheTechnicalMusingsCafe class with the necessary details and calls the requestTable method for each customer.
public class CafeHungerManager {
public static final Logger logger = LoggerFactory.getLogger(CafeHungerManager.class);
public static void main(String[] args) {
logger.info("🍽️ The Technical Musings Cafe's CafeHungerManager Booting Up...");
logger.info("🚀 Preparing tables for incoming hungry customers...");
int totalTables = 5, diningTime = 10, noOfCustomersArrived = 12;
TheTechnicalMusingsCafe cafe = new TheTechnicalMusingsCafe(totalTables, diningTime);
IntStream.rangeClosed(1, noOfCustomersArrived)
.mapToObj(i -> "Customer-" + i)
.forEach(cafe::requestTable);
}
}
Here, we’ve assumed that there are only five tables in the cafe. Also, it’s a rush hour, so 12 customers have arrived at the same time. Therefore, the Cafe’s hunger manager has requested tables for each of the 12 customers.
Next, let’s build the core logic for allocating tables to the customers in the TheTechnicalMusingsCafe class.
Initializing the Semaphore
First, we’ll define a few class-level variables and initialize them in the constructor:
private final Semaphore tables;
private final LinkedBlockingQueue<String> waitingQueue;
private final long diningTime;
public TheTechnicalMusingsCafe(int tableCount, int diningTime) {
this.tables = new Semaphore(tableCount, true);
this.waitingQueue = new LinkedBlockingQueue<>();
this.diningTime = diningTime;
}
Aside Semaphore, we are also using a queue to ensure the FIFO (first-in, first-out) order for customers. Moreover, the diningTime field refers to the average time it takes a customer to enjoy their meal once they get a table.
Most importantly, we have initialized the Semaphore with two parameters: the first specifies the number of permits (in this case, tables) that are initially available, the second, when set to true, ensures that permits are granted on a FIFO basis.
Although we’ve set the fairness attribute of the Semaphore to true, it only helps to ensure that threads acquire permits in the order they requested, but it does not strictly enforce FIFO execution when multiple threads are competing for access. Therefore, we’re using a queue in our example.
Requesting Table for the Customer to Dine
With this, let’s now move on to writing the code for the requestTable method:
public void requestTable(String customerName) {
int customerPositionInQueue = waitingQueue.size();
int availableTables = tables.availablePermits();
long waitTime = (availableTables > 0)
? (customerPositionInQueue / availableTables) * diningTime
: customerPositionInQueue * diningTime;
logger.info("⚡ {} enters, evaluating their hunger. Estimated wait:: {} seconds.", customerName, waitTime);
waitingQueue.offer(customerName);
new Thread(this::dine).start();
}
As we can see, this method calculates the wait time for each customer (yes! We love to walk the extra mile), adds the customer to the queue, and then calls the dine method.
The real magic of Semaphore happens inside the dine method. This is where we’ll use the Semaphore to ensure the tables are offered to the customers as and when they are available.
Managing Tables with Semaphore
So, let’s write the final and most important piece of the code – the dine method:
public void dine() {
String customerName = "unknown";
try {
customerName = waitingQueue.take();
tables.acquire();
logger.info("✅ {} got a table and is enjoying their coffee☕ with code\uD83D\uDC68\u200D\uD83D\uDCBB.", customerName);
Thread.sleep(diningTime * 1000);
} catch (InterruptedException e) {
logger.error("🚨 {} faced unexpected hunger-induced interruption!", Thread.currentThread().getName(), e);
} finally {
logger.info("\uD83D\uDEAA {} finished dining \uD83D\uDE0A and left, freeing a table.", customerName);
tables.release();
}
}
As you can see, it is very simple to use a Semaphore. In the dine method, we first take the customer thread from the queue. Next, we call the acquire method of the Semaphore.
The Semaphore will allow the thread only when there are permits available. Also, when a thread acquires a permit, it must release it. Therefore, in the finally section, we’re calling release on the Semaphore to ensure that the permit is made available for the next thread (or shall I say Customer).
Cafe’s Code in Action
Now, finally, let’s run the Java code we’ve written and see how smoothly our Cafe Hunger Manager is doing its job.
Let’s spend some time going through the logs to understand how it works.
13:28:43.968 [main] 🍽️ The Technical Musings Cafe's CafeHungerManager Booting Up...
13:28:43.972 [main] 🚀 Preparing tables for incoming hungry customers...
13:28:43.977 [main] ⚡ Customer-1 enters, evaluating their hunger. Estimated wait:: 0 seconds.
13:28:43.977 [main] ⚡ Customer-2 enters, evaluating their hunger. Estimated wait:: 0 seconds.
13:28:43.977 [Thread-0] ✅ Customer-1 got a table and is enjoying their coffee☕ with code👨💻.
13:28:43.977 [main] ⚡ Customer-3 enters, evaluating their hunger. Estimated wait:: 0 seconds.
13:28:43.983 [Thread-1] ✅ Customer-2 got a table and is enjoying their coffee☕ with code👨💻.
13:28:43.983 [main] ⚡ Customer-4 enters, evaluating their hunger. Estimated wait:: 0 seconds.
13:28:43.983 [Thread-2] ✅ Customer-3 got a table and is enjoying their coffee☕ with code👨💻.
13:28:43.983 [main] ⚡ Customer-5 enters, evaluating their hunger. Estimated wait:: 0 seconds.
13:28:43.983 [Thread-3] ✅ Customer-4 got a table and is enjoying their coffee☕ with code👨💻.
13:28:43.983 [main] ⚡ Customer-6 enters, evaluating their hunger. Estimated wait:: 10 seconds.
13:28:43.983 [Thread-4] ✅ Customer-5 got a table and is enjoying their coffee☕ with code👨💻.
13:28:43.983 [main] ⚡ Customer-7 enters, evaluating their hunger. Estimated wait:: 10 seconds.
13:28:43.983 [main] ⚡ Customer-8 enters, evaluating their hunger. Estimated wait:: 20 seconds.
13:28:43.983 [main] ⚡ Customer-9 enters, evaluating their hunger. Estimated wait:: 20 seconds.
13:28:43.983 [main] ⚡ Customer-10 enters, evaluating their hunger. Estimated wait:: 20 seconds.
13:28:43.983 [main] ⚡ Customer-11 enters, evaluating their hunger. Estimated wait:: 20 seconds.
13:28:43.983 [main] ⚡ Customer-12 enters, evaluating their hunger. Estimated wait:: 20 seconds.
13:28:53.995 [Thread-4] 🚪 Customer-5 finished dining 😊 and left, freeing a table.
13:28:53.995 [Thread-2] 🚪 Customer-3 finished dining 😊 and left, freeing a table.
13:28:53.995 [Thread-3] 🚪 Customer-4 finished dining 😊 and left, freeing a table.
13:28:53.995 [Thread-5] ✅ Customer-6 got a table and is enjoying their coffee☕ with code👨💻.
13:28:53.996 [Thread-1] 🚪 Customer-2 finished dining 😊 and left, freeing a table.
13:28:53.996 [Thread-7] ✅ Customer-8 got a table and is enjoying their coffee☕ with code👨💻.
13:28:53.996 [Thread-6] ✅ Customer-7 got a table and is enjoying their coffee☕ with code👨💻.
13:28:53.996 [Thread-8] ✅ Customer-9 got a table and is enjoying their coffee☕ with code👨💻.
13:28:54.030 [Thread-0] 🚪 Customer-1 finished dining 😊 and left, freeing a table.
13:28:54.030 [Thread-9] ✅ Customer-10 got a table and is enjoying their coffee☕ with code👨💻.
13:29:04.011 [Thread-7] 🚪 Customer-8 finished dining 😊 and left, freeing a table.
13:29:04.017 [Thread-10] ✅ Customer-11 got a table and is enjoying their coffee☕ with code👨💻.
13:29:04.019 [Thread-8] 🚪 Customer-9 finished dining 😊 and left, freeing a table.
13:29:04.019 [Thread-11] ✅ Customer-12 got a table and is enjoying their coffee☕ with code👨💻.
13:29:04.019 [Thread-5] 🚪 Customer-6 finished dining 😊 and left, freeing a table.
13:29:04.011 [Thread-6] 🚪 Customer-7 finished dining 😊 and left, freeing a table.
13:29:04.041 [Thread-9] 🚪 Customer-10 finished dining 😊 and left, freeing a table.
13:29:14.030 [Thread-11] 🚪 Customer-12 finished dining 😊 and left, freeing a table.
13:29:14.030 [Thread-10] 🚪 Customer-11 finished dining 😊 and left, freeing a table.
Process finished with exit code 0
See, how organised everything is now – thanks to Semaphore.
Wrapping Up: Semaphores Beyond the Technical Musings Cafe
So, now that our cafe operation is running smoothly because of Semaphore, let’s check some real-world applications of Semaphore:
- Database Connection Pooling: Limit the number of concurrent database connections
- Rate Limiting API Requests: Restrict the number of concurrent API calls
- Traffic Light Control Systems: Manage vehicle flow at intersections by allowing only a limited number of cars to pass at a time
- Parking Lot Management Systems: Restricts cars entering a parking lot based on available parking space
Conclusion
To summarise, Semaphores may sound complex, but as we saw with The Technical Musings Cafe, they’re just a smart way to manage shared resources efficiently.
Moreover, whether in multithreading, traffic control, or everyday systems, they keep things running smoothly. So next time you see a busy cafe, you might just spot a semaphore in action!
If you liked this article, do check out more articles with code examples at the Code Katas section of my blog.