在我们刚学Java的时候,程序在main中自上而下、顺序执行。后来在实习中接触到Java多线程的应用,多线程充分利用了单台服务器的计算资源,然后就根据服务器的load和cpu核数来调整程序的线程数。
当时对多线程可能了解不太得深入,只能通过服务器负载来调整现成的数量。从当时的应用效果来看,对于线程之间不需要进行交互的应用场景来说(例如处理Kafka的数据),多线程技术真的是简单好用。
但是对于线程之间需要交互的场景来说,例如Thread A、B、C共同完成一个工作,如果A干完了需要等待B、C一起完成,如果自己实现的话就比较麻烦,所以今天就来介绍能够满足这样场景的并发工具类:CountDownLatch 和 CyclicBarrier。
CountDownLatch可以理解成倒计时计数器,它可以让多个线程等待某些任务完成后再继续执行。如果你以前遇到过“等所有子任务执行完再汇总结果”的问题。
我们使用 new CountDownLatch(N)创建对象时,构造函数中的N就表示计数器,这里和线程数是一致的。
对于开发者来说,我们只关注两个方法:
如果不想无限等待,可以指定超时时间:
latch.await(5, TimeUnit.SECONDS);
上面代码表示,如果 5 秒内 countDown()
没有执行完,await()
会自动超时并继续执行。
这里举个例子,假设你是一个老师,考试结束后,你必须等所有学生交卷(countDown()
)后,才能收卷走人(await()
)。
我们先定义一个学生类Student:
class Student implements Runnable {
private CountDownLatch latch;
private int id;
public Student(CountDownLatch latch, int id) {
this.latch = latch;
this.id = id;
}
@Override
public void run() {
System.out.println("学生 " + id + " 交卷");
latch.countDown(); // 交卷后,计数器减 1
}
}
在Student类中,通过构造参数传入CountDownLatch,并实现Runnable线程类的run(),通过调用countDown() 完成N - 1来模拟学生的交卷动作。然后实现一个主类,主线程main就扮演老师的角色。
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
int studentCount = 5;
CountDownLatch latch = new CountDownLatch(studentCount);
for (int i = 1; i <= studentCount; i++) {
new Thread(new Student(latch, i)).start();
}
// 等待所有学生交卷(N变成0)
latch.await();
System.out.println("老师收卷,考试结束!");
}
}
在代码中创建五个学生(线程),然后启动程序。我们知道,在main中的代码是顺序执行的,正常情况下,创建并启动线程之后,main函数是接着执行的,也就是说,我不管学生交卷与否,我直接收卷结束考试。
但是在上面main函数中,添加了一行代码 latch.await() ,也就意味着必须要等到CountDownLatch中的N变成0(即所有人交卷),才能继续执行,如图所示:
我们知道多线程的情况下,每个线程执行完成的顺序是没有规律的,但是不论谁先完成都要等待。
在多线程并发工具中,除了CountDownLatch之外,CyclicBarrier也具有这种”等待“功能,CyclicBarrier翻译为循环屏障,但与CountDownLatch的使用上游不同之处:
特性 | CountDownLatch | CyclicBarrier |
---|---|---|
计数方式 | 只减不加,不能重用 | 可重置,循环使用 |
线程等待 |
|
|
适用场景 | 某个任务要等待多个线程完成 | 多个线程需要同步执行某个任务 |
简单来说:
可能上面说的很难理解,我们先从用法开始看起:
从用法中可以看出,await 既能使计数器 -1,还能等待,那么CountDownLatch中,计数器-1是在线程中调用的,那么我们就在线程中调用await()。
class Student implements Runnable {
private CyclicBarrier cyclicBarrier;
private int id;
public Student(CyclicBarrier cyclicBarrier, int id) {
this.cyclicBarrier = cyclicBarrier;
this.id = id;
}
@SneakyThrows
@Override
public void run() {
System.out.println("学生 " + id + " 交卷");
cyclicBarrier.await();
}
}
此时,我们在main线程中也调用await等待所有线程完成。
public static void main(String[] args) throws InterruptedException {
int studentCount = 5;
CyclicBarrier cyclicBarrier = new CyclicBarrier(studentCount + 1);
for (int i = 1; i <= studentCount; i++) {
new Thread(new Student(cyclicBarrier, i)).start();
}
cyclicBarrier.await();
System.out.println("老师收卷,考试结束!");
}
与CountDownLatch不同的是,有5个学生,但是这里的N是6,因为老师也要等待所有学生完成之后,自己才能结束考试。
同样一个交卷场景下,CountDownLatch用的就挺舒服,但是CyclicBarrier就比较别扭,因为你想,考试的情况下,明明只要老师等待所有人交卷结束考试即可,为什么学生之间还需要等待呢?
所以CyclicBarrier比较适合的场景是:学生互相等待一起做一件事情。同样是考试,老师要求必须所有学生到齐了才能发卷考试,我们将上面run()中的“交卷”改为“到达教室”,然后实现main函数。
public static void main(String[] args) {
int studentCount = 5;
CyclicBarrier cyclicBarrier = new CyclicBarrier(studentCount, () -> {
System.out.println("开始考试");
});
for (int i = 1; i <= studentCount; i++) {
new Thread(new Student(cyclicBarrier, i)).start();
}
}
}
在上面main中无需调用await等待。在CyclicBarrier我们除了传入N,还可以传入一个回调函数,当N为0时,会执行回调函数。所以CyclicBarrie更适合线程之间等待的场景,代码执行结果如下:
什么时候用 CountDownLatch?
什么时候用 CyclicBarrier?
CountDownLatch参与主体是线程和主线程,线程之间不需要等待,执行任务的主体主线程。CyclicBarrier参与主体只有线程,线程之间需要互相等待,执行任务的主体是线程本身。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。