回答
CyclicBarrier
和 CountdownLatch
都是 Java 并发编程中的同步辅助类。
CyclicBarrier
:允许一组线程互相等待,直到所有线程都达到了一个共同的屏障点。CountdownLatch
:允许一个或多个线程等待,直到在其他线程中进行的一组操作完成为止。
实际上两者之间还是有一点点相同,都是一组线程等待某一个触发点完成后才能进行后续的操作。其主要区别有:
1、设计理念不同
CyclicBarrier
:用于多个线程间的互相等待,直到所有线程都达到了一个共同的屏障点。它主要用于多个线程需要等待彼此达到某个状态后才一起继续执行的场景。比如 6 个人约去开会,只有 6 个全到了才能开会,先到的需要等待后面的同学。CountdownLatch
:用于一个或多个线程等待其他线程完成一定的操作。它主要用于某个线程需要等待一个任务集合完成后才开始执行的场景。
2、重用性不同
CyclicBarrier
:可重用,一旦所有等待线程都释放后,它可以被重置并用于新的屏障点。CountDownLatch
:不能重用,一旦计数器达到零,它就不能再次使用。
3、屏蔽动作不同
CyclicBarrier
:可以指定一个屏障动作,这是一个Runnable,当所有线程都到达屏障时自动执行。CountDownLatch
:没有屏障动作的概念,只是简单地允许一组线程等待直到计数器减为零。
4、应用场景不同
CyclicBarrier
:适用于多个线程之间需要互相等待并在到达某一点之后进行后续操作的场景,比如多个线程都到达了起点后再同时起跑。CountDownLatch
:适用于一个线程需要等待多个线程完成某个操作之后才能继续执行的场景,比如在某个任务执行之前,需要先执行几个预处理任务,这时就可以使用CountDownLatch将预处理任务和主任务串行化。
核心原理分析
CyclicBarrier 的核心原理
CyclicBarrier
描述允许一组线程相互等待,直到所有线程都达到一个公共屏障点(barrier point),才会进行后续任务。因为该 barrier 在释放等待线程后可以重用,所以称它为循环的 barrier。
在创建 CyclicBarrier
的时候会设定一个整数 parties
,parties
表示屏障拦截的线程数量,每当一个线程调用 await()
方法时,就等于告诉 CyclicBarrier
它已经到达了屏障,并且当前线程会被阻塞。
当 parties 个线程都调用了 await()
方法,就表示所有线程都已到达屏障。这时,CyclicBarrier
会释放所有等待的线程,然后这些线程才能继续执行。
在构造 CyclicBarrier
时,我们可以指定一个 Runnable。在屏障条件达成,所有等待线程被释放前,这个命令会在一个线程中执行一次。
举一个简单的例子,比如我们有 5 个小组要讨论3 个议题,只有当 5 个小组全部都讨论完成后,才能进行下一个议题。
public class CyclicBarrierTest {
public static void main(String[] args) {
int parties = 5;
CyclicBarrier cyclicBarrier = new CyclicBarrier(parties,
() -> System.out.println( Thread.currentThread().getName() + " - 所有小组都完成了当前议题的讨论,可以开始下一个议题。"));
for (int i = 0; i < parties; i++) {
new Thread(() -> {
try {
// 讨论第一个议题
System.out.println("小组 [" + Thread.currentThread().getName() + "] 正在讨论第一个议题;;" + LocalTime.now());
TimeUnit.SECONDS.sleep(new Random().nextInt(10));
System.out.println("小组 [" + Thread.currentThread().getName() + "] 已完成讨论第一个议题" + LocalTime.now());
cyclicBarrier.await();
// 讨论第二个议题
System.out.println("小组 [" + Thread.currentThread().getName() + "] 正在讨论第二个议题" + LocalTime.now());
TimeUnit.SECONDS.sleep(new Random().nextInt(10));
System.out.println("小组 [" + Thread.currentThread().getName() + "] 已完成讨论第二个议题" + LocalTime.now());
cyclicBarrier.await();
// 讨论第三个议题
System.out.println("小组 [" + Thread.currentThread().getName() + "] 正在讨论第三个议题" + LocalTime.now());
TimeUnit.SECONDS.sleep(new Random().nextInt(10));
System.out.println("小组 [" + Thread.currentThread().getName() + "] 已完成讨论第三个议题" + LocalTime.now());
cyclicBarrier.await();
System.out.println("小组 [" + Thread.currentThread().getName() + "] 完成了所有议题的讨论。" + LocalTime.now());
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
为了模拟每个线程的耗时,大明哥增加了随机 sleep(),这样就可以保证每个线程到达屏障的时间点不一样,执行结果:
小组 [Thread-2] 正在讨论第一个议题;;14:59:25.066181
小组 [Thread-1] 正在讨论第一个议题;;14:59:25.066206
小组 [Thread-0] 正在讨论第一个议题;;14:59:25.066216
小组 [Thread-3] 正在讨论第一个议题;;14:59:25.066213
小组 [Thread-4] 正在讨论第一个议题;;14:59:25.066171
小组 [Thread-1] 已完成讨论第一个议题14:59:29.084990
小组 [Thread-0] 已完成讨论第一个议题14:59:29.084990
小组 [Thread-3] 已完成讨论第一个议题14:59:31.083911
小组 [Thread-4] 已完成讨论第一个议题14:59:31.083854
小组 [Thread-2] 已完成讨论第一个议题14:59:32.081640
Thread-2 - 所有小组都完成了当前议题的讨论,可以开始下一个议题。
小组 [Thread-2] 正在讨论第二个议题14:59:32.082730
小组 [Thread-4] 正在讨论第二个议题14:59:32.082804
小组 [Thread-1] 正在讨论第二个议题14:59:32.082761
小组 [Thread-3] 正在讨论第二个议题14:59:32.082795
小组 [Thread-0] 正在讨论第二个议题14:59:32.082776
小组 [Thread-0] 已完成讨论第二个议题14:59:32.083250
小组 [Thread-4] 已完成讨论第二个议题14:59:33.083757
小组 [Thread-3] 已完成讨论第二个议题14:59:37.085017
小组 [Thread-2] 已完成讨论第二个议题14:59:39.084345
小组 [Thread-1] 已完成讨论第二个议题14:59:40.083453
Thread-1 - 所有小组都完成了当前议题的讨论,可以开始下一个议题。
小组 [Thread-1] 正在讨论第三个议题14:59:40.083704
小组 [Thread-0] 正在讨论第三个议题14:59:40.083780
小组 [Thread-2] 正在讨论第三个议题14:59:40.083917
小组 [Thread-4] 正在讨论第三个议题14:59:40.083845
小组 [Thread-3] 正在讨论第三个议题14:59:40.083872
小组 [Thread-1] 已完成讨论第三个议题14:59:40.084163
小组 [Thread-2] 已完成讨论第三个议题14:59:44.089334
小组 [Thread-0] 已完成讨论第三个议题14:59:46.086578
小组 [Thread-4] 已完成讨论第三个议题14:59:46.086579
小组 [Thread-3] 已完成讨论第三个议题14:59:48.088779
Thread-3 - 所有小组都完成了当前议题的讨论,可以开始下一个议题。
小组 [Thread-1] 完成了所有议题的讨论。14:59:48.089335
小组 [Thread-0] 完成了所有议题的讨论。14:59:48.089511
小组 [Thread-3] 完成了所有议题的讨论。14:59:48.089274
小组 [Thread-2] 完成了所有议题的讨论。14:59:48.089424
小组 [Thread-4] 完成了所有议题的讨论。14:59:48.089592
针对结果,各位小伙伴可以好好研究下!
关于 CyclicBarrier
的源码,大明哥的这篇文章已经做了非常详细的说明:https://www.skjava.com/series/article/9494645068
CountDownLatch
CountDownLatch
所描述的在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
CountDownLatch
的核心是一个计数器,在构建 CountDownLatch 对象的时候,我们传入一个计数器 count,该值就表示了线程的数量。
- 线程等待:当一个线程调用
await()
时,它会一直等待,直到计数器变为 0。 - 计数器减少:当某个线程完成工作后,会调用
countDown()
表示任务已完成,此时计数器会减 1,当计数器的值减至 0 时,所有等待在await()
方法上的线程都会被释放,可以继续执行。
还是开会议例子,会议有一些需要准备的工作,比如:设置会议室、准备茶点、准备演示文稿,只有当这些准备工作准备完成后,会议才会开始。
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(3);
// 设置会议室
new Thread(() -> {
System.out.println("正在设置会议室...;;" + LocalTime.now());
try {
TimeUnit.SECONDS.sleep(new Random().nextInt(10));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
countDownLatch.countDown();
System.out.println("会议室设置完成...;;" + LocalTime.now());
}).start();
// 准备演示文稿
new Thread(() -> {
System.out.println("正在准备演示文稿..." + LocalTime.now());
try {
TimeUnit.SECONDS.sleep(new Random().nextInt(10));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
countDownLatch.countDown();
System.out.println("演示文稿准备完成...;;" + LocalTime.now());
}).start();
// 准备茶点
new Thread(() -> {
System.out.println("正在准备茶点...;;" + LocalTime.now());
try {
TimeUnit.SECONDS.sleep(new Random().nextInt(10));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
countDownLatch.countDown();
System.out.println("茶点准备完成...;;" + LocalTime.now());
}).start();
// 等待准备工作完成
System.out.println("主持人正在等待准备工作完成...;;" + LocalTime.now());
countDownLatch.await();
System.out.println("所有准备工作完成,会议可以开始了...;;"+ LocalTime.now());
}
}
与 CyclicBarrier
一样,也是采用 sleep() 随机数来模拟线程完成的时间。执行结果如下:
正在设置会议室...;;15:28:00.892957
主持人正在等待准备工作完成...;;15:28:00.892985
正在准备演示文稿...15:28:00.892987
正在准备茶点...;;15:28:00.892957
会议室设置完成...;;15:28:04.898727
茶点准备完成...;;15:28:04.898726
演示文稿准备完成...;;15:28:06.896947
所有准备工作完成,会议可以开始了...;;15:28:06.897028
关于 CountDownLatch
源码,大明哥的这篇文章有详细的分析:
https://www.skjava.com/series/article/1687215113
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。