Lesson 3
Implementing a Restaurant Ordering System with Semaphores
Implementing a Restaurant Ordering System with Semaphores

Welcome back! In our previous lesson, we examined how to simulate a car manufacturing line with multiple synchronized stages using Phaser and ExecutorService. That exercise helped illustrate the power of managing thread coordination in complex tasks. Today, we're shifting focus to another practical application of Java concurrency: implementing a restaurant ordering system using semaphores. This lesson aims to build on your concurrency knowledge, emphasizing resource management and thread synchronization.

What You'll Learn

In this lesson, you will explore:

  • The concept of resource management through semaphores.
  • How to handle concurrency when multiple threads access limited resources concurrently.
  • Implementing a practical simulation of a real-life scenario where semaphores manage resource constraints effectively.
Recap: Using Semaphores and Their Role in Resource Management

You’ve previously learned about semaphores as a concurrency mechanism that helps control access to limited resources. A semaphore allows only a certain number of threads to access a resource at any given time. When a thread completes its task, it releases its hold on the semaphore, allowing another thread to proceed. This is particularly useful when simulating real-world systems where resources (like chefs in a restaurant) are limited.

In this lesson, we will use semaphores to simulate a restaurant with multiple waiters taking orders and a limited number of chefs preparing the orders. The semaphore will ensure that only a specified number of orders are being prepared simultaneously, mimicking the constraint of having a limited number of chefs. The ExecutorService will manage the waiters, who submit orders to be processed.

Setting Up the Restaurant System

Let's first set up the basic structure of our system. We begin by initializing the semaphore and the executor service:

Java
1import java.util.concurrent.ExecutorService; 2import java.util.concurrent.Executors; 3import java.util.concurrent.Semaphore; 4import java.util.concurrent.TimeUnit; 5 6public class RestaurantOrderingSystem { 7 8 private final Semaphore chefSemaphore; 9 private final ExecutorService executor; 10 11 public RestaurantOrderingSystem(int numberOfChefs, int numberOfWaiters) { 12 this.chefSemaphore = new Semaphore(numberOfChefs); 13 this.executor = Executors.newFixedThreadPool(numberOfWaiters); 14 } 15}

In the above code, the Semaphore is initialized with the number of available chefs, controlling how many orders can be processed simultaneously. ExecutorService manages multiple threads representing waiters.

Here’s what the key methods in the above snippet do:

  • newFixedThreadPool(): This method creates a fixed pool of threads where the number of waiters taking orders is controlled.
  • Semaphore: Manages the number of chefs available to process orders at a time, ensuring that only the allowed number of orders are prepared simultaneously.
Taking and Processing Orders

Now, let's move on to the logic for taking and processing orders. When an order is taken by a waiter, the system submits the task to the executor, and the semaphore controls access to the chefs who can prepare the orders.

Java
1public void takeOrder(int orderNumber) { 2 executor.submit(() -> { 3 String waiterName = Thread.currentThread().getName(); 4 try { 5 System.out.println(waiterName + " takes Order-" + orderNumber); 6 chefSemaphore.acquire(); // Ensure a chef is available 7 System.out.println("Chef starts preparing Order-" + orderNumber + " taken by " + waiterName); 8 prepareOrder(orderNumber); 9 System.out.println("Chef completes Order-" + orderNumber + " taken by " + waiterName); 10 } catch (InterruptedException e) { 11 Thread.currentThread().interrupt(); 12 System.err.println(waiterName + " was interrupted while taking Order-" + orderNumber); 13 } finally { 14 chefSemaphore.release(); // Release the chef for other orders 15 } 16 }); 17}

In the above code, the following actions occur:

  • Submitting the Task: The order is submitted to the executor via submit(), which ensures a thread is allocated to handle the task of taking and preparing the order.
  • Semaphore Acquisition: The semaphore’s acquire() method ensures that the number of available chefs is checked before the order is processed. If all chefs are busy, the thread will wait until a chef becomes available.
  • Releasing the Semaphore: After the order is prepared, the chef is "freed" with release(), allowing another order to be processed.

The chefSemaphore.acquire() and chefSemaphore.release() methods control access to the limited number of chefs (or resources) available for processing orders.

Preparing Orders and Shutting Down the System

Let’s now complete the logic for preparing the order and shutting down the executor when all tasks are finished:

Java
1private void prepareOrder(int orderNumber) { 2 try { 3 Thread.sleep((long) (Math.random() * 3000 + 1000)); // Simulates order preparation time 4 } catch (InterruptedException e) { 5 Thread.currentThread().interrupt(); 6 System.err.println("Preparation of Order-" + orderNumber + " was interrupted."); 7 } 8} 9 10public void shutdown() { 11 executor.shutdown(); 12 try { 13 if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { 14 executor.shutdownNow(); 15 if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { 16 System.err.println("Executor did not terminate."); 17 } 18 } 19 } catch (InterruptedException e) { 20 executor.shutdownNow(); 21 Thread.currentThread().interrupt(); 22 } 23 System.out.println("Restaurant is closed. All orders have been processed."); 24}

Here’s what happens:

  • prepareOrder(): This simulates the time it takes to prepare an order. The sleep() method introduces a delay that mimics the order preparation process.
  • shutdown(): This method gracefully shuts down the executor service once all orders are completed. It ensures that no further tasks are submitted, and it waits for all ongoing tasks to finish before shutting down completely.

The shutdown() method is critical for ensuring that all orders are processed before the system shuts down.

Why It Matters

In this lesson, we used semaphores to simulate a restaurant ordering system, demonstrating how concurrency control works in a scenario where limited resources must be managed efficiently. By applying semaphores to control how many chefs can work at the same time, we’ve created a realistic simulation of how tasks can be regulated and managed in real-world applications, such as restaurants, call centers, or any scenario where limited resources must be shared among many tasks.

Managing limited resources using semaphores is a key skill in software development, especially when building systems that must handle multiple tasks simultaneously with controlled access to shared resources.

Now that you understand how to implement this system, let’s move to the practice section, where you’ll apply these concepts in hands-on tasks to further solidify your knowledge.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.