1.概念介绍
CountDownLatch 是一个计数器,计数器的初始值由创建它时指定。每次调用 countDown() 方法时,计数器会减1,直到计数器值变为0时,所有调用 await() 的线程都会被唤醒继续执行。
CyclicBarrier 是 Java 中另一个常用的同步辅助工具,它允许一组线程互相等待,直到所有线程都达到一个共同的屏障点(barrier)。与 CountDownLatch 不同的是,CyclicBarrier 可以被重用,即当所有线程都达到了屏障点并执行完相应任务后,CyclicBarrier 可以再次使用。CyclicBarrier 的主要优势在于它的重用性和灵活性,特别适用于循环性或分阶段的任务场景。
• 重用:CyclicBarrier 可以重用,而 CountDownLatch 是一次性的,计数器不能重置。
• 目的:CountDownLatch 用于让一个或多个线程等待其他线程完成,而 CyclicBarrier 是让一组线程互相等待,直到所有线程都到达一个屏障点。
2. 简单举例
1) CountDownLatch 简单示例:
import java.util.concurrent.CountDownLatch;public class CountDownLatchExample {public static void main(String[] args) throws InterruptedException {int numberOfTasks = 3;CountDownLatch latch = new CountDownLatch(numberOfTasks);// 创建并启动任务for (int i = 0; i < numberOfTasks; i++) {new Thread(new Task(latch)).start();}// 主线程等待所有任务完成latch.await();System.out.println("所有任务已完成,继续主线程的操作。");}
}class Task implements Runnable {private CountDownLatch latch;public Task(CountDownLatch latch) {this.latch = latch;}@Overridepublic void run() {try {// 模拟任务执行时间Thread.sleep((long) (Math.random() * 1000));System.out.println(Thread.currentThread().getName() + " 任务完成");} catch (InterruptedException e) {e.printStackTrace();} finally {latch.countDown(); // 任务完成后计数器减1}}
}
解释
• CountDownLatch latch = new CountDownLatch(3);:初始化 CountDownLatch,计数器值为3。
• 每个线程在执行完任务后调用 latch.countDown();,计数器减1。
• latch.await(); 让主线程等待,直到计数器变为0,也就是所有任务完成。
CountDownLatch 非常适合用在需要等多个线程执行完毕后再执行下一步操作的场景,比如并行任务的同步。
2) CountDownLatch 有漏洞的比赛示例(有漏洞):
本实验想要实现的是裁判员要等待所有运动员各就各位后全部准备完毕,再开始比赛的效果。
创建测试用的项目代码如下:
package service;import java.util.concurrent.CountDownLatch;public class MyService {private CountDownLatch down = new CountDownLatch(1);public void testMethod() {try {System.out.println(Thread.currentThread().getName() + "准备");down.await();System.out.println(Thread.currentThread().getName() + "结束");} catch (InterruptedException e) {e.printStackTrace();}}public void downMethod() {System.out.println("开始");down.countDown();}}
线程类MyThread.java代码如下:
package extthread;import service.MyService;public class MyThread extends Thread {private MyService myService;public MyThread(MyService myService) {super();this.myService = myService;}@Overridepublic void run() {myService.testMethod();}}
运行类Run.java代码如下:
package test.run;import service.MyService;
import extthread.MyThread;public class Run {public static void main(String[] args) throws InterruptedException {MyService service = new MyService();MyThread[] tArray = new MyThread[10];for (int i = 0; i < tArray.length; i++) {tArray[i] = new MyThread(service);tArray[i].setName("线程" + (i + 1));tArray[i].start();}Thread.sleep(2000);service.downMethod();}
}
程序运行结果如下图所示:
此实验虽然运行成功,但并不能保证在main主线程中执行了service.downMethod();时,所有的工作线程都呈wait状态,因为某些线程有可能准备的时间花费较长,可能耗用的时间超过2秒,这时如果在第2秒时调用service.downMethod();方法就达不到“唤醒所有线程”继续向下运行的目的了,也就是说裁判员没有等全部的运动员到来时,就让发令枪响起开始比赛了,这是不对的,所以就需要对代码进行修改,来达到相对比较完善的比赛流程。
漏洞:countDown()方法的调用时机在所有任务完成任务的时间点之前,导致问题。
改进:等待10个线程要用CountDownLatch(10), 或者用CyclicBarrier。
2) CountDownLatch 完整的比赛示例:
使用CountDownLatch类来实现“所有的线程”呈wait后再统一唤醒的效果,通过大量使用CountDownLatch类来实现业务要求的同步效果。
创建实验用的项目,代码如下:
package extthread;import java.util.concurrent.CountDownLatch;public class MyThread extends Thread {private CountDownLatch comingTag; // 裁判等待所有运动员到来private CountDownLatch waitTag; // 等待裁判说准备开始private CountDownLatch waitRunTag; // 等待起跑private CountDownLatch beginTag; // 起跑private CountDownLatch endTag; // 所有运动员到达终点public MyThread(CountDownLatch comingTag, CountDownLatch waitTag,CountDownLatch waitRunTag, CountDownLatch beginTag,CountDownLatch endTag) {super();this.comingTag = comingTag;this.waitTag = waitTag;this.waitRunTag = waitRunTag;this.beginTag = beginTag;this.endTag = endTag;}@Overridepublic void run() {try {System.out.println("运动员使用不同交通工具不同速度到达起跑点,正向这头走!");Thread.sleep((int) (Math.random() * 10000));System.out.println(Thread.currentThread().getName() + "到起跑点了!");comingTag.countDown();System.out.println("等待裁判说准备!");waitTag.await();System.out.println("各就各位!准备起跑姿势!");Thread.sleep((int) (Math.random() * 10000));waitRunTag.countDown();beginTag.await();System.out.println(Thread.currentThread().getName()+ " 运行员起跑 并且跑赛过程用时不确定");Thread.sleep((int) (Math.random() * 10000));endTag.countDown();System.out.println(Thread.currentThread().getName() + " 运行员到达终点");} catch (InterruptedException e) {e.printStackTrace();}}}
运行类Run.java代码如下:
package test;import java.util.concurrent.CountDownLatch;
import extthread.MyThread;public class Run {public static void main(String[] args) {try {CountDownLatch comingTag = new CountDownLatch(10);CountDownLatch waitTag = new CountDownLatch(1);CountDownLatch waitRunTag = new CountDownLatch(10);CountDownLatch beginTag = new CountDownLatch(1);CountDownLatch endTag = new CountDownLatch(10);MyThread[] threadArray = new MyThread[10];for (int i = 0; i < threadArray.length; i++) {threadArray[i] = new MyThread(comingTag, waitTag, waitRunTag,beginTag, endTag);threadArray[i].start();}System.out.println("裁判员在等待选手的到来!");comingTag.await();System.out.println("裁判看到所有运动员来了,各就各位前“巡视”用时5秒");Thread.sleep(5000);waitTag.countDown();System.out.println("各就各位!");waitRunTag.await();Thread.sleep(2000);System.out.println("发令枪响起!");beginTag.countDown();endTag.await();System.out.println("所有运动员到达,统计比赛名次!");} catch (InterruptedException e) {e.printStackTrace();}}}
运动员使用不同交通工具不同速度到达起跑点,正向这头走!
运动员使用不同交通工具不同速度到达起跑点,正向这头走!
运动员使用不同交通工具不同速度到达起跑点,正向这头走!
运动员使用不同交通工具不同速度到达起跑点,正向这头走!
运动员使用不同交通工具不同速度到达起跑点,正向这头走!
运动员使用不同交通工具不同速度到达起跑点,正向这头走!
运动员使用不同交通工具不同速度到达起跑点,正向这头走!
运动员使用不同交通工具不同速度到达起跑点,正向这头走!
运动员使用不同交通工具不同速度到达起跑点,正向这头走!
裁判员在等待选手的到来!
运动员使用不同交通工具不同速度到达起跑点,正向这头走!
Thread-9到起跑点了!
等待裁判说准备!
Thread-2到起跑点了!
等待裁判说准备!
Thread-7到起跑点了!
等待裁判说准备!
Thread-5到起跑点了!
等待裁判说准备!
Thread-4到起跑点了!
等待裁判说准备!
Thread-0到起跑点了!
等待裁判说准备!
Thread-1到起跑点了!
等待裁判说准备!
Thread-3到起跑点了!
等待裁判说准备!
Thread-8到起跑点了!
等待裁判说准备!
Thread-6到起跑点了!
等待裁判说准备!
裁判看到所有运动员来了,各就各位前“巡视”用时5秒
各就各位!
各就各位!准备起跑姿势!
各就各位!准备起跑姿势!
各就各位!准备起跑姿势!
各就各位!准备起跑姿势!
各就各位!准备起跑姿势!
各就各位!准备起跑姿势!
各就各位!准备起跑姿势!
各就各位!准备起跑姿势!
各就各位!准备起跑姿势!
各就各位!准备起跑姿势!
发令枪响起!
Thread-9 运行员起跑 并且跑赛过程用时不确定
Thread-1 运行员起跑 并且跑赛过程用时不确定
Thread-4 运行员起跑 并且跑赛过程用时不确定
Thread-7 运行员起跑 并且跑赛过程用时不确定
Thread-8 运行员起跑 并且跑赛过程用时不确定
Thread-5 运行员起跑 并且跑赛过程用时不确定
Thread-0 运行员起跑 并且跑赛过程用时不确定
Thread-2 运行员起跑 并且跑赛过程用时不确定
Thread-3 运行员起跑 并且跑赛过程用时不确定
Thread-6 运行员起跑 并且跑赛过程用时不确定
Thread-2 运行员到达终点
Thread-8 运行员到达终点
Thread-4 运行员到达终点
Thread-5 运行员到达终点
Thread-9 运行员到达终点
Thread-7 运行员到达终点
Thread-6 运行员到达终点
Thread-3 运行员到达终点
Thread-0 运行员到达终点
Thread-1 运行员到达终点
所有运动员到达,统计比赛名次!
4) CyclicBarrier 简单示例:
每次当所有线程都到达屏障点(即调用 await()),屏障就会被“破坏”,所有等待的线程继续执行,CyclicBarrier 会自动重置为初始状态,允许它再次被使用。这个过程可以无限次重复。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;public class CyclicBarrierReuseExample {public static void main(String[] args) {int numberOfThreads = 3;CyclicBarrier barrier = new CyclicBarrier(numberOfThreads, new Runnable() {@Overridepublic void run() {System.out.println("所有线程都已到达屏障点,继续下一阶段...");}});// 启动3个线程,它们将经过5个阶段,每个阶段有3个屏障点for (int i = 0; i < numberOfThreads; i++) {new Thread(new Task(barrier)).start();}}
}class Task implements Runnable {private CyclicBarrier barrier;public Task(CyclicBarrier barrier) {this.barrier = barrier;}@Overridepublic void run() {try {for (int i = 0; i < 5; i++) { // 让线程经过5个阶段,模拟5个阶段System.out.println(Thread.currentThread().getName() + " 正在执行阶段 " + (i + 1) + "...");Thread.sleep((long) (Math.random() * 1000)); // 模拟任务处理时间System.out.println(Thread.currentThread().getName() + " 到达屏障点 " + (i + 1));barrier.await(); // 等待其他线程到达屏障点// 继续执行下一阶段}} catch (InterruptedException | BrokenBarrierException e) {e.printStackTrace();}}
}
Thread-0 正在执行阶段 1...
Thread-1 正在执行阶段 1...
Thread-2 正在执行阶段 1...
Thread-0 到达屏障点 1
Thread-1 到达屏障点 1
Thread-2 到达屏障点 1
所有线程都已到达屏障点,继续下一阶段...
Thread-2 正在执行阶段 2...
Thread-1 正在执行阶段 2...
Thread-0 正在执行阶段 2...
...
所有线程都已到达屏障点,继续下一阶段...
Thread-0 正在执行阶段 5...
Thread-1 正在执行阶段 5...
Thread-2 正在执行阶段 5...
Thread-2 到达屏障点 5
Thread-0 到达屏障点 5
Thread-1 到达屏障点 5
所有线程都已到达屏障点,继续下一阶段...
解释:
• 重用:for (int i = 0; i < 5; i++) 循环中,每个线程都会经过5次屏障点。每当所有线程都到达屏障点并通过时,CyclicBarrier 会自动重置,使得它可以在下一轮循环中继续使用。
• 屏障点:在每个阶段,所有线程都需要等到其他线程都到达相同的屏障点,然后再继续执行。这模拟了多阶段任务中同步的场景。
• 线程同步:在每个屏障点,线程都调用 barrier.await() 来等待其他线程,直到所有线程都到达,这时屏障才会被解除,线程们才继续执行。
• 自动重置:在每个阶段的屏障点解除后,CyclicBarrier 自动重置,以便在下一个阶段再次使用。
通过这个例子可以看出,CyclicBarrier 的重用特性非常适合于那些需要分阶段执行的任务,确保每个阶段都在所有线程同步完成后再开始下一个阶段。