Java CyclicBarrier: Synchronize Parallel Tasks
java cyclicbarrier: Learn how to use Java CyclicBarrier to coordinate parallel threads, handle barrier failures, and choose between CyclicBarrier and CountDownLatch.
java cyclicbarrier requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
The Purpose of CyclicBarrier
In concurrent Java applications, threads often need to wait for each other before proceeding with a shared phase of computation. CyclicBarrier is a synchronization construct that lets a fixed number of threads wait for each other to reach a common barrier point. Once all threads arrive, the barrier releases them simultaneously, and the barrier can be reused for the next phase. This is useful for parallel algorithms that split work into stages, such as iterative solvers or multi-threaded data processing pipelines.
Unlike a simple latch, a CyclicBarrier is designed for repeated use. After all threads have been released, the barrier resets automatically, allowing the same set of threads to synchronize again in a subsequent round.
How CyclicBarrier Works Internally
A CyclicBarrier is constructed with a party count and an optional barrier action. When a thread calls await(), it blocks until the required number of threads have called await(). When the last thread arrives, the barrier action (if any) runs, and all waiting threads are released. The barrier then resets to its initial state.
Internally, CyclicBarrier uses a lock and condition to manage the waiting threads. Each await() call decrements an internal count. When the count reaches zero, the barrier trips. If a thread is interrupted or the barrier is broken, BrokenBarrierException is thrown to the waiting threads.
A Minimal CyclicBarrier Example
Consider a scenario where three worker threads must each compute a partial result, then combine them after all have finished. The following code demonstrates a simple use of CyclicBarrier:
import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; public class BarrierExample { public static void main(String[] args) { int parties = 3; CyclicBarrier barrier = new CyclicBarrier(parties, () -> System.out.println("All threads reached the barrier. Combining results.")); Runnable worker = () -> { String name = Thread.currentThread().getName(); System.out.println(name + " started computing."); // Simulate work try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println(name + " finished computing, waiting at barrier."); try { barrier.await(); } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } System.out.println(name + " passed the barrier."); }; for (int i = 0; i < parties; i++) { new Thread(worker).start(); } } }
When all three threads call await(), the barrier action prints a message, and then all threads continue. The output shows that each thread waits until the last one arrives.
Handling BrokenBarrierException and InterruptedException
await() can throw two checked exceptions. InterruptedException is thrown if the calling thread is interrupted while waiting. BrokenBarrierException is thrown when the barrier is broken, which happens if one of the waiting threads is interrupted, or if a timeout occurs, or if the barrier is reset while threads are waiting. When the barrier breaks, all other waiting threads receive BrokenBarrierException and the barrier becomes unusable until reset.
A common mistake is to ignore these exceptions. In production code, you should decide how to handle a broken barrier: either abort the computation or reset the barrier and retry. For example:
try { barrier.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // Handle interruption, possibly abort } catch (BrokenBarrierException e) { // Barrier broken, decide whether to reset or fail }
Using Timeouts and Resetting a Barrier
CyclicBarrier provides an overloaded await(long timeout, TimeUnit unit) method. If the timeout elapses before all parties arrive, the barrier is considered broken, and all waiting threads receive BrokenBarrierException. This is useful for preventing indefinite blocking.
The reset() method manually breaks the barrier and returns it to its initial state. Any threads currently waiting will receive BrokenBarrierException. After reset(), the barrier can be reused. However, reset() should be used carefully, because it can cause unexpected failures in threads that are still waiting.
CyclicBarrier vs CountDownLatch
A common point of confusion is when to use CyclicBarrier versus CountDownLatch. The key difference is reusability. A CountDownLatch is a one-shot gate: once the count reaches zero, it cannot be reused. A CyclicBarrier is reusable and supports an optional barrier action.
| Feature | CyclicBarrier | CountDownLatch |
|---|---|---|
| Reusability | Reusable after all threads arrive | One-time use only |
| Barrier action | Optional Runnable runs when tripped | No such mechanism |
| Exception handling | Throws BrokenBarrierException | No equivalent |
| Use case | Multi-phase parallel computation | Waiting for a single event |
Use CyclicBarrier when threads must synchronize at multiple points during a computation. Use CountDownLatch when you need to wait for one or more events to complete, such as waiting for a set of services to start.
When to Use CyclicBarrier in Real Applications
CyclicBarrier is well suited for parallel algorithms that iterate over a dataset in stages. For example, in a multi-threaded image processing pipeline, each thread processes a portion of an image, then they synchronize to combine results before moving to the next filter. Another example is a parallel numerical solver where each iteration requires all threads to finish before the next iteration begins.
The overhead of using a barrier is relatively low compared to the cost of thread coordination, but it is not zero. If the barrier is used excessively in a tight loop, the synchronization cost can become a bottleneck. In such cases, consider whether a different coordination pattern, such as a fork-join pool, might be more efficient.
Also, keep in mind that the number of threads must match the party count. If the party count is set incorrectly, threads will block indefinitely. It is important to ensure that the party count reflects the actual number of threads that will call await().