1. Introduction
In this tutorial, we're going to look at how we can use CyclicBarrier in Java. Primarily, a CyclicBarrier allows threads to wait for other threads at a barrier point. It serves a similar purpose as a CountDownLatch but unlike a CountDownLatch, we can use it multiple times.
2. CyclicBarrier Usage
A CyclicBarrier is initialized with a count representing the number of attending parties/threads. When a thread invokes the await() method, it blocks until all other threads invoke await(). When this happens, the barrier is reset and the threads are released.
Another important feature of CyclicBarrier is that we can supply a barrier action. It runs after all threads reach the barrier point but before they're allowed to continue.
Let's examine its usage through an example:
public class Worker implements Runnable {
private final CyclicBarrier start;
Worker(CyclicBarrier start) {
this.start = start;
}
public void run() {
try {
System.out.println("Ready to start.");
start.await();
doWork();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
System.out.println("Interrupted.");
} catch (BrokenBarrierException ex) {
System.out.println("Broken barrier.");
}
}
public void doWork() {
System.out.println("Doing work.");
}
}
Here, we have the Worker class which implements Runnable. Note that it's expecting a CyclicBarrier - start - in the constructor. Then in the run method, it's invoking the start.await() method before doing some work. At this point, our worker thread gets blocked until all other threads also invoke start.await().
Next, we'll execute some Worker tasks:
public void startBarrier() throws InterruptedException {
final int threadCount = 3;
final ExecutorService threadPool = Executors.newFixedThreadPool(threadCount);
final CyclicBarrier start = new CyclicBarrier(threadCount,
() -> System.out.println("All ready to continue!"));
for (int i = 0; i < threadCount; ++i) {
threadPool.execute(new Worker(start));
}
TimeUnit.SECONDS.sleep(1); // Wait for some time
threadPool.shutdown();
}
Here, we're creating a CyclicBarrier with a count of three and a barrier action. Then we're running three worker tasks initialized with this barrier. Since all workers must reach the start barrier before they can do any work, the first two tasks will block until the third one also reaches the barrier. In the end, three await() invocations occurring on three different threads release all attending threads.
A sample run shows the coordinated execution:
Ready to start.
Ready to start.
Ready to start.
All ready to continue!
Doing work.
Doing work.
Doing work.
Note that the given barrier action runs before the threads are released.
3. Use the Same CyclicBarrier Multiple Times
Whenever a group of threads reaches a barrier point, the barrier is implicitly reset. This allows us to use the same CyclicBarrier multiple times to coordinate a group of threads in different phases of computation.
For this purpose, we'll build upon the previous example. At first, the threads will wait for each other to start their execution. Then when all participating threads invoke await(), they'll continue and do their work. Lastly, to complete their execution, they'll synchronize again. In the end, the same set of threads will meet at two different barrier points using the same CyclicBarrier.
Firstly, we'll modify Worker:
public class Worker implements Runnable {
private final CyclicBarrier barrier;
Worker(CyclicBarrier barrier) {
this.barrier = barrier;
}
public void run() {
try {
System.out.println("Ready to start.");
barrier.await();
doWork();
barrier.await();
System.out.println("Done.");
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
System.out.println("Interrupted.");
} catch (BrokenBarrierException ex) {
System.out.println("Broken barrier.");
}
}
public void doWork() {
System.out.println("Doing work.");
}
}
In this modified version of Worker, we're invoking barrier.await() two times - with the additional invocation after doWork(). As a result, we're creating two barrier points in which all threads must wait for each other to continue.
A sample run - using the previous task execution code - shows the coordination between worker threads:
Ready to start.
Ready to start.
Ready to start.
All ready to continue!
Doing work.
Doing work.
Doing work.
All ready to continue!
Done.
Done.
Done.
4. CyclicBarrier vs CountDownLatch
Now, we'll talk about the differences between CyclicBarrier and CountDownLatch.
Firstly, a CyclicBarrier can be used multiple times. A CountDownLatch, on the other hand, is single-use only. Once the latch reaches its terminal state, all CountDownLatch.await() invocations return immediately.
Secondly, CyclicBarrier needs only one type of thread, whereas CountDownLatch requires two types of thread. In other words, to make use of CyclicBarrier, there must be some threads invoking await(). To use CountDownLatch, on the other hand, there must be some threads invoking await() and also others invoking countDown(). In this regard, CountDownLatch waits for events via countDown() whereas CyclicBarrier waits for threads via await().
Lastly, if we need to perform some action after all tasks reach a barrier point - like merging the results, publishing notifications - barrier actions can create a big difference. CountDownLatch doesn't provide this sort of action out of the box.
5. Summary
In this tutorial, we've investigated how we can use CyclicBarrier to allow threads coordinate their operations.
Check out the source code for all examples in this tutorial over on Github.