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.
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.
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.
Let's first set up the basic structure of our system. We begin by initializing the semaphore and the executor service:
Java1import 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.
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.
Java1public 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.
Let’s now complete the logic for preparing the order and shutting down the executor when all tasks are finished:
Java1private 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. Thesleep()
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.
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.