Welcome back to our advanced journey into concurrency! In the previous lesson, we explored semaphores to manage access to shared resources. Today, we will build on that foundation by learning how to coordinate threads using CyclicBarrier. This tool helps synchronize threads so they meet at a common point before proceeding, which is crucial in complex multithreaded applications.
In this lesson, you will discover:
- The concept and use cases of CyclicBarrier.
- How to synchronize threads at a common barrier point.
- Setting up parties along with barrier actions.
By the end, you'll be able to manage thread coordination efficiently, ensuring that groups of threads reach designated synchronization points before continuing their tasks.
A CyclicBarrier is a synchronization aid that allows a set of threads to wait for each other to reach a common barrier point. It's particularly useful in programs involving a fixed group of threads that need to coordinate at certain stages of execution. The barrier is called "cyclic" because it can be reused after the threads waiting at the barrier have been released.
For example, you might use a CyclicBarrier
in situations where a set of tasks must all reach a certain point before any can proceed. After all the threads arrive at the barrier, they are released together, and the barrier can be reused if needed.
Key Features include:
-
Barrier Point Synchronization: Threads call the
await()
method to signal they've reached the barrier point, and they block until all threads reach the barrier. -
Reusability: Once all threads reach the barrier, it resets, allowing it to be reused for multiple cycles.
-
Optional Barrier Action: A task can be executed when all threads have reached the barrier. This is often used for tasks that depend on the collective progress of all threads, such as combining results or setting up the next stage of computation.
CyclicBarriers are ideal in scenarios like simulations, where phases of tasks must be coordinated, or in games, where multiple players or entities must wait for each other at certain points before progressing.
Let’s explore an example where a group of hikers must reach a summit before continuing their journey. Each hiker is simulated as a thread, and they will wait at the summit using a CyclicBarrier
until all other hikers arrive. The barrier ensures no hiker can proceed until the entire group has gathered, providing synchronization at a key point in the journey.
This simulates real-world scenarios where tasks must synchronize at specific stages before moving forward. Let’s start by implementing the Hiker
class.
We begin by defining the Hiker
class. Each hiker is represented by a thread, and the class must hold a reference to the shared CyclicBarrier
so each hiker knows when to wait for the others.
Java1import java.util.concurrent.CyclicBarrier; 2 3public class Hiker implements Runnable { 4 private CyclicBarrier barrier; 5 6 public Hiker(CyclicBarrier barrier) { 7 this.barrier = barrier; 8 } 9}
In this part of the class, the constructor takes a CyclicBarrier
object and assigns it to the barrier
field. This ensures that all Hiker
instances (threads) will use the same CyclicBarrier
to coordinate their movements. By having each thread aware of the barrier, they can synchronize their progress at certain stages of the hike.
Next, let’s implement the core logic for the Hiker
class. Each hiker will hike to the base camp, hike to the summit (where they will wait for all other hikers), and then hike back down.
Java1import java.util.concurrent.BrokenBarrierException; 2 3@Override 4public void run() { 5 try { 6 // Hike to Base Camp (no waiting at this point) 7 hike("Base Camp"); 8 9 // Hike to Summit (wait at the summit for all hikers) 10 hike("Summit"); 11 barrier.await(); // Wait at the summit for all other hikers to arrive 12 13 // Continue the journey after all have arrived at the summit 14 hike("Back to Base Camp"); 15 16 System.out.println(Thread.currentThread().getName() + " has completed the hike."); 17 18 } catch (InterruptedException | BrokenBarrierException e) { 19 e.printStackTrace(); 20 } 21} 22 23private void hike(String destination) throws InterruptedException { 24 System.out.println(Thread.currentThread().getName() + " is hiking to " + destination + "."); 25 Thread.sleep((long) (Math.random() * 3000) + 1000); // Simulate hiking time 26 System.out.println(Thread.currentThread().getName() + " has arrived at " + destination + "."); 27}
Let's break down what the above code does:
-
Hiking Process: The
hike()
method simulates the process of hiking by printing a message and then pausing the thread for a random period (usingThread.sleep()
) to represent travel time. Each thread has its own pace, so the sleep duration varies. -
Barrier Synchronization: After hiking to the summit, each hiker calls
barrier.await()
. This causes the thread to pause and wait until all other hikers have also calledawait()
. Once all threads reach this point, the barrier opens, and they can continue hiking. -
Exception Handling: The
run()
method includes handling forInterruptedException
andBrokenBarrierException
to ensure the application handles interruptions and issues with the barrier properly.
This structure models how hikers pause and wait for each other before continuing. It ensures that no one proceeds until all hikers have reached the summit.
Now, let's set up the Main
class to simulate a group of hikers reaching the summit. The CyclicBarrier
will make sure all hikers wait for each other at the summit before proceeding.
Java1import java.util.concurrent.CyclicBarrier; 2 3public class Main { 4 public static void main(String[] args) { 5 final int NUM_HIKERS = 5; // Number of hikers 6 7 // Create a CyclicBarrier with a barrier action 8 CyclicBarrier barrier = new CyclicBarrier(NUM_HIKERS, new Runnable() { 9 @Override 10 public void run() { 11 // This task executes once all hikers reach the summit 12 System.out.println("\nAll hikers have reached the summit! Let's take a group photo!\n"); 13 } 14 }); 15 16 // Create and start hiker threads 17 for (int i = 1; i <= NUM_HIKERS; i++) { 18 new Thread(new Hiker(barrier), "Hiker " + i).start(); 19 } 20 } 21}
Here's what we're doing in the Main
class:
-
Creating the CyclicBarrier: We initialize a
CyclicBarrier
with 5 hikers. The second parameter is a barrier action that runs when all hikers reach the summit. In this case, it prints a message indicating that everyone has reached the summit, and they can now take a group photo. -
Starting Threads: We loop over the number of hikers, creating a new thread for each one using the
Hiker
class. Each thread starts immediately after creation, simulating the hikers’ journeys to the summit. The barrier ensures that no hiker can complete the journey without waiting for all other hikers to arrive at the same point.
This class brings together all the threads and coordinates their progress using the CyclicBarrier
.
Mastering coordination with CyclicBarrier
is essential for developers handling complex synchronization scenarios. You can manage threads effectively, ensuring that tasks reach synchronization points before proceeding. This is particularly useful in applications like simulations, where various tasks must synchronize at key stages, or in games, where phases of a game might depend on all players reaching certain checkpoints.
Key Advantages of CyclicBarrier:
-
Structured Synchronization: Threads can reach a barrier and wait for others before continuing, ensuring smooth coordination.
-
Reusable for Multiple Phases: After all threads reach the barrier, it resets, making it useful for tasks that involve several stages or phases.
-
Optional Actions on Completion: A barrier action can execute tasks like combining results when all threads have reached the barrier.
By understanding how to implement CyclicBarrier
, you can ensure that threads cooperate and wait for each other, enabling more structured and reliable thread management.
With this foundation, you’re now equipped to coordinate threads effectively using CyclicBarrier. Get ready to apply these learnings in the upcoming practice section. Happy coding!