Lesson 4
Traffic Signal Simulation Using CyclicBarrier
Traffic Signal Simulation with CyclicBarrier

Welcome back! In the last lesson, we learned how to manage resource allocation in a restaurant ordering system using semaphores. In this lesson, we’re going to apply similar synchronization principles using CyclicBarrier to simulate a traffic signal system. This lesson will deepen your understanding of task synchronization by coordinating the actions of multiple threads that depend on each other.

What You'll Learn

In this lesson, we’ll explore:

  • How to use CyclicBarrier to coordinate the behavior of multiple threads.
  • Implementing a traffic signal system at a four-way intersection.
  • How to manage thread execution with ExecutorService and cyclic synchronization.

By the end of this lesson, you’ll know how to apply CyclicBarrier in real-world scenarios like traffic management, where tasks need to synchronize before proceeding.

Recap: CyclicBarrier and Its Role in Synchronization

You’ve encountered various concurrency tools in previous lessons, such as semaphores for managing limited resources. CyclicBarrier is different—it’s a synchronization aid that makes multiple threads wait for each other before continuing their tasks. This is especially useful when actions must be synchronized, like cars waiting for a green light at an intersection.

In this lesson, we’ll use CyclicBarrier to simulate a traffic signal system. Cars from different directions (threads) arrive at the intersection and must wait for each other before they can proceed. The barrier makes sure that no car proceeds until all have arrived at the intersection, just like how traffic lights coordinate cars at a busy junction.

Setting Up the Traffic Signal System

We begin by defining the core structure for the traffic signal simulation. This includes setting up the CyclicBarrier and managing threads using ExecutorService.

Java
1import java.util.concurrent.BrokenBarrierException; 2import java.util.concurrent.CyclicBarrier; 3import java.util.concurrent.ExecutorService; 4import java.util.concurrent.Executors; 5import java.util.concurrent.TimeUnit; 6 7public class TrafficSignalSimulation { 8 9 private final CyclicBarrier barrier; 10 private final ExecutorService executor; 11 private final int totalCycles; 12 13 public TrafficSignalSimulation(int numDirections, int totalCycles) { 14 this.barrier = new CyclicBarrier(numDirections, this::changeTrafficLights); 15 this.executor = Executors.newFixedThreadPool(numDirections); 16 this.totalCycles = totalCycles; 17 } 18}

In this snippet:

  • CyclicBarrier: This is initialized with the number of directions (cars) needing synchronization. It waits for all cars to arrive at the intersection before allowing any of them to proceed.
  • ExecutorService: Manages the threads representing cars arriving from different directions. Each car (thread) moves independently, but is synchronized using the barrier.
  • totalCycles: This determines how many times the cars will go through the intersection before the simulation ends.
Java
1this.barrier = new CyclicBarrier(numDirections, this::changeTrafficLights);

Here, once all threads reach the barrier, the changeTrafficLights method is executed. This action simulates the event of traffic lights changing to allow cars to proceed through the intersection.

With this setup, we have the basic structure to simulate a traffic signal system where cars wait for each other at the intersection.

Simulating Car Movement

Next, we simulate the movement of cars through the intersection, making them wait for others using the CyclicBarrier.

Java
1public void simulate(int direction) { 2 executor.submit(new Car(direction, barrier, totalCycles)); 3} 4 5private static class Car implements Runnable { 6 private final int direction; 7 private final CyclicBarrier barrier; 8 private final int totalCycles; 9 10 public Car(int direction, CyclicBarrier barrier, int totalCycles) { 11 this.direction = direction; 12 this.barrier = barrier; 13 this.totalCycles = totalCycles; 14 } 15 16 @Override 17 public void run() { 18 String threadName = "Car-" + direction; 19 for (int cycle = 1; cycle <= totalCycles; cycle++) { 20 try { 21 System.out.println(threadName + " arrives at the intersection (Cycle " + cycle + ")."); 22 barrier.await(); // Cars wait at the red light 23 System.out.println(threadName + " proceeds through the intersection (Cycle " + cycle + ")."); 24 Thread.sleep((long) (Math.random() * 1000 + 500)); // Simulate crossing time 25 } catch (InterruptedException e) { 26 Thread.currentThread().interrupt(); 27 System.err.println(threadName + " was interrupted."); 28 break; 29 } catch (BrokenBarrierException e) { 30 System.err.println(threadName + " encountered a broken barrier."); 31 break; 32 } 33 } 34 System.out.println(threadName + " has completed all cycles."); 35 } 36}

In this block, we simulate car movement at an intersection using the Car class, which implements Runnable. Each car is modeled as a separate thread.

  • simulate(int direction): Initializes the simulation for a car from a specific direction, submitting it as a task to the ExecutorService to run in a separate thread.

  • Car Class:

    • Fields:

      • direction: Identifies the car's arriving direction.
      • barrier: The CyclicBarrier instance used for synchronization among car threads.
      • totalCycles: The number of simulation cycles for each car at the intersection.
    • run() Method:

      • Cycle Loop: The car announces its arrival for each cycle.
      • barrier.await(): The car waits synchronously with others. The barrier ensures all cars wait before proceeding, akin to a red light.
      • Thread.sleep(): Simulates the time taken to cross the intersection, introducing a pause.
      • Exception Handling:
        • InterruptedException: Handles thread interruptions by setting the interrupt status and breaking the loop.
        • BrokenBarrierException: Indicates a disrupted barrier; prints an error and exits the loop.
      • Completion Message: Prints a message when the car finishes all cycles.

This block uses CyclicBarrier for synchronized starts, ensuring all cars proceed together, similar to traffic light operations at intersections. This structure ensures that no car proceeds through the intersection until all have arrived, mimicking real-life traffic lights.

Changing the Traffic Lights

Now, let’s define the action that happens when all cars have arrived at the intersection and the barrier breaks—changing the traffic lights.

Java
1private void changeTrafficLights() { 2 System.out.println("\n=== Traffic lights have changed. Cars can now proceed. ===\n"); 3}

This method is automatically invoked when the barrier is broken (i.e., when all cars are ready to proceed). It mimics the changing of traffic lights, signaling that it’s safe for the cars to continue.

Shutting Down the Simulation

Finally, after all the cars have completed their cycles, we need to shut down the simulation properly.

Java
1public void shutdown() { 2 executor.shutdown(); 3 try { 4 if (!executor.awaitTermination(totalCycles * 10, TimeUnit.SECONDS)) { 5 executor.shutdownNow(); 6 if (!executor.awaitTermination(10, TimeUnit.SECONDS)) { 7 System.err.println("Executor did not terminate."); 8 } 9 } 10 } catch (InterruptedException e) { 11 executor.shutdownNow(); 12 Thread.currentThread().interrupt(); 13 } 14 System.out.println("Traffic signal simulation ended."); 15}
  • executor.shutdown(): Ensures that all running tasks (car threads) finish before shutting down the system.
  • awaitTermination(): Waits for all threads to complete. If they don’t finish in the allotted time, the executor is forcefully terminated.

This ensures that the simulation doesn't terminate prematurely, and all threads complete their tasks safely.

Why It Matters

Simulating a traffic signal system using CyclicBarrier helps you understand task synchronization, which is crucial in many real-world systems. Here’s why this concept is important:

  • Synchronizing Tasks: The barrier ensures that tasks proceed together, preventing any task from getting ahead before the others are ready.
  • Real-world Application: This mirrors real-world traffic management systems, where coordination is essential to avoid collisions and ensure safety.
  • Concurrency Control: With CyclicBarrier, you can manage tasks that must operate in lockstep, making it useful for scenarios like simulations, parallel computing, or even multiplayer games.

Now that you’ve seen how CyclicBarrier is used to synchronize tasks in a traffic simulation, let's move to the practice section and apply these concepts. You’ll have the chance to reinforce your understanding with similar synchronization challenges!

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