前言
在Java的并发编程领域,Java并发包(java.util.concurrent)为开发者提供了丰富的同步工具类,旨在有效解决多线程协作中的各类复杂问题。这些工具类不仅增强了程序对并发场景的处理能力,还显著提升了代码的可读性和可维护性。本文将深入探讨两种尤为常用的同步辅助类:CyclicBarrier与CountDownLatch。CyclicBarrier作为线程间同步的屏障,确保所有参与线程在达到某个共同点时继续执行。而CountDownLatch则如同一道计数器门闩,允许一个或多个线程等待其他线程完成一系列操作。
一、CyclicBarrier 与 CountDownLatch介绍
1. CyclicBarrier
CyclicBarrier 是一个同步辅助类,它允许一组线程相互等待,直到所有线程都到达某个公共的屏障点(即同步点)。这个机制在需要多线程协调完成某个任务时非常有用,特别是在所有线程都必须等待彼此完成某些前置工作才能继续执行后续任务的情况下。
特性:
- 可重用性:与 CountDownLatch 不同,CyclicBarrier 的屏障点是可以重复使用的。一旦所有线程都通过了屏障点,屏障会被自动重置,以便后续线程可以再次使用。
- 回调机制:CyclicBarrier 允许在所有线程到达屏障点时执行一个可选的屏障动作(通过 Runnable 实现)。这个机制可以用于执行一些在所有线程都准备好后需要进行的初始化工作。
- 线程管理:CyclicBarrier 还提供了对参与线程的灵活管理,包括处理因中断或超时而未能到达屏障点的线程。
应用场景:
- 多线程并行计算中的阶段同步。
- 分布式系统中的节点同步。
- 并发任务中的资源准备和初始化。
2. CountDownLatch
CountDownLatch 是一个同步辅助类,它实现了一个计数器,该计数器的操作是原子性的。它允许一个或多个线程等待其他线程完成一系列操作。
特性:
- 一次性使用:CountDownLatch 的计数器一旦减至0,就不能再被重置。这意味着它通常用于一次性的事件或任务同步。
- 原子操作:计数器的递减操作是线程安全的,确保了多线程环境下的正确性。
- 灵活性:通过构造函数可以设置初始计数值,这允许灵活地控制需要等待的线程数量。
应用场景:
- 等待一组操作完成后再继续执行。
- 在多线程环境中协调任务的启动顺序。
- 在测试框架中模拟资源准备或条件满足前的等待。
使用注意事项:
- 当 CountDownLatch 的计数器达到0时,所有等待的线程都会被立即释放,因此需要注意避免竞态条件。
- 由于 CountDownLatch 是一次性的,如果需要重复使用的同步机制,应考虑使用 CyclicBarrier。
3. CyclicBarrier与CountDownLatch的主要区别
在可重用性方面,CyclicBarrier展现出了其独特的优势。与CountDownLatch的一次性使用特性不同,CyclicBarrier的屏障点可以反复被利用。一旦所有线程都通过了CyclicBarrier设定的屏障点,该屏障会被自动重置,为后续的线程同步提供持续的服务。这意味着,在需要多次进行线程同步的场景中,CyclicBarrier是更为合适的选择。
在等待模式上,CountDownLatch与CyclicBarrier则存在着显著的差异。CountDownLatch允许一个或多个线程等待,直至某个计数器减至零,这通常代表着一组事件的全部完成。相比之下,CyclicBarrier则是设计用于让一组线程相互等待,直至所有线程都达到某个同步点。这种等待模式使得CyclicBarrier在需要多线程共同协作完成某个任务时,能够发挥出色的协调作用。
二、案例
1. CyclicBarrier 案例
在Java并发编程中,CyclicBarrier是一个功能强大的同步辅助类,它特别适用于需要一组固定大小的线程不时地互相等待的场景。假设有一个任务,需要四个线程共同参与,每个线程在任务执行到某个关键点时,都需要等待其他三个线程也到达这个点,然后才能继续执行后续的任务。这种情况下,CyclicBarrier就是一个非常合适的选择。
java
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
// 创建一个CyclicBarrier对象,设置屏障点为4,表示需要4个线程到达屏障点
CyclicBarrier barrier = new CyclicBarrier(4, new Runnable() {
@Override
public void run() {
System.out.println("所有线程都已到达屏障点,可以继续执行后续任务");
}
});
// 创建并启动四个线程
for (int i = 0; i < 4; i++) {
new Thread(new Worker(barrier, i)).start();
}
}
static class Worker implements Runnable {
private CyclicBarrier barrier;
private int id;
public Worker(CyclicBarrier barrier, int id) {
this.barrier = barrier;
this.id = id;
}
@Override
public void run() {
try {
System.out.println("线程 " + id + " 正在执行任务");
// 模拟任务执行过程
Thread.sleep((long) (Math.random() * 1000));
// 到达屏障点,等待其他线程
barrier.await();
// 所有线程都已到达屏障点,继续执行后续任务
System.out.println("线程 " + id + " 继续执行后续任务");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
在main方法中创建了一个CyclicBarrier对象,并设置了屏障点为4,这意味着需要4个线程都到达屏障点后,才能继续执行。同时传入了一个Runnable对象作为屏障动作,当所有线程都到达屏障点时,这个Runnable的run方法会被执行。
使用一个for循环创建了四个Worker线程,并启动了它们,每个Worker线程在执行任务的过程中,会调用barrier.await()方法来等待其他线程。在Worker的run方法中,每个线程首先打印一条消息表示它正在执行任务,然后模拟了一个随机的任务执行时间(通过Thread.sleep),之后线程调用barrier.await()方法等待其他线程。
当所有线程都到达屏障点时,CyclicBarrier会执行传入的屏障动作(打印一条消息表示所有线程都已到达屏障点),并且它们会跳出await方法的阻塞状态,继续执行后续的任务(打印一条消息表示继续执行后续任务)。
运行结果:

通过这个案例可以看到CyclicBarrier在多线程协作中的重要作用,它允许一组线程在达到某个同步点时互相等待,从而确保所有线程都准备好后再继续执行后续的任务。
2. CountDownLatch 的应用案例
在Java并发编程中,CountDownLatch是一个重要的同步辅助类,它常用于一个任务需要等待其他多个任务完成后再继续执行的场景。假设有一个主任务,它依赖于三个子任务的完成。为了确保主任务在所有子任务都完成后才开始执行,可以使用CountDownLatch,将CountDownLatch的初始计数值设置为3,表示需要等待三个子任务的完成。
java
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CountDownLatchExample {
public static void main(String[] args) {
// 创建一个CountDownLatch对象,初始计数值为3
CountDownLatch latch = new CountDownLatch(3);
// 创建一个线程池来执行子任务
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 提交三个子任务到线程池
for (int i = 0; i < 3; i++) {
executorService.submit(new SubTask(latch, i));
}
// 创建并启动主任务线程
new Thread(new MainTask(latch)).start();
// 关闭线程池
executorService.shutdown();
}
static class SubTask implements Runnable {
private CountDownLatch latch;
private int id;
public SubTask(CountDownLatch latch, int id) {
this.latch = latch;
this.id = id;
}
@Override
public void run() {
System.out.println("子任务 " + id + " 正在执行");
// 模拟子任务执行过程
try {
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子任务 " + id + " 完成,调用 latch.countDown()");
latch.countDown();
}
}
static class MainTask implements Runnable {
private CountDownLatch latch;
public MainTask(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
try {
System.out.println("主任务正在等待子任务完成");
latch.await();
System.out.println("所有子任务已完成,主任务继续执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在main方法中创建了一个CountDownLatch对象,并设置了初始计数值为3,这个计数值表示需要等待三个子任务的完成。并且使用Executors.newFixedThreadPool(3)创建了一个固定大小的线程池,用于执行三个子任务,通过循环将三个SubTask对象提交到线程池中。在SubTask的run方法中,每个子任务首先打印一条消息表示它正在执行,然后模拟了一个随机的任务执行时间(通过Thread.sleep)。任务完成后,调用latch.countDown()方法将CountDownLatch的计数值减1。
在MainTask的run方法中,主任务首先打印一条消息表示它正在等待子任务的完成。然后调用latch.await()方法阻塞当前线程,直到CountDownLatch的计数值变为0(即所有子任务都已完成)。当所有子任务都完成后,CountDownLatch的计数值变为0,主任务线程从await方法的阻塞状态中恢复,继续执行后续的任务(打印一条消息表示主任务继续执行)。
运行结果:

通过这个案例可以看到CountDownLatch在协调多个任务完成顺序方面的重要作用,它允许一个任务等待其他多个任务的完成,从而确保任务之间的正确执行顺序。
总结
CyclicBarrier 和 CountDownLatch 是 Java 并发包(java.util.concurrent)中至关重要的同步辅助类,它们在多线程编程中发挥着关键作用。开发者可以根据具体的应用场景和需求,选择合适的工具来实现线程间的协作与同步。深入理解这两种同步机制的工作原理、用法及其区别,对于有效解决多线程编程中遇到的同步问题至关重要,这不仅提升了代码的可维护性和可读性,也显著增强了程序的稳定性和性能。