本文举例说明了如何在Java中使用CountDownLatch来处理多线程应用程序中线程之间的协调。
从Java 5开始,核心的Java APIs得到了增强,增加了更多处理并发编程中线程间协调的特性。在本文中,我们将讨论java.util.concurrent包中有助于实现这一目的的一个类闭锁.
介绍
CountDownLatch类使我们能够通过引入对正在执行相关任务的线程数量的感知并跟踪已经完成其任务的线程数量来协调线程。
这是通过用工作线程的数量初始化CountDownLatch来完成的。每个工作线程都应该调用countDown()CountDownLatch上的方法。需要等待工作线程完成的线程应该调用await()CountDownLatch上的方法。这将导致该线程一直等待,直到所有工作线程都调用了countDown()方法,实际上是从工作线程数递减到零。一旦倒计时到达零,调用await()可以继续。
例子
下面的示例说明了如何在两种不同的情况下使用CountDownLatch。
场景1:等待线程指示一组任务的完成
在这个例子中,让我们考虑一个场景,其中许多比萨饼制造商制作一些比萨饼。由不同的比萨饼制造商制作的比萨饼然后被收集起来交付给饥饿的顾客。
为了实现这一点,我们使用一个线程来代表每一个比萨饼制造商。这些东西越多,同时做的披萨就越多。下面是PizzaMaker班级。
interface FoodMaker {
void make();
}
class PizzaMaker implements FoodMaker, Runnable {
int numberOfPizzas;
List<Pizza> pizzas;
CountDownLatch countDownLatch;
public PizzaMaker(int numberOfPizzas, CountDownLatch countDownLatch) {
this.numberOfPizzas = numberOfPizzas;
this.countDownLatch = countDownLatch;
}
public List<Pizza> getPizzas() {
if(pizzas == null || pizzas.isEmpty())
return null;
return pizzas.stream().filter((pizza -> pizza.isPrepared())).collect(Collectors.toList());
}
@Override
public void make() {
pizzas = new ArrayList<>();
for(int i=0 ; i < numberOfPizzas; i++) {
try {
Pizza pizza = new Pizza();
Thread.sleep(1000);
pizza.setPrepared(true);
pizzas.add(pizza);
System.out.println(Thread.currentThread().getName() + " - Pizza ready!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
countDownLatch.countDown();
}
@Override
public void run() {
this.make();
}
}
请注意,构造函数接受一个CountDownLatch。这将由等待完成的调用线程传入。每个PizzaMaker实例将需要调用countDown()方法来指示其工作的完成。如上面的代码所示,这是在make()方法一旦所有的比萨饼都做好了。
下面的代码演示了如何使用CountDownLatch以及如何阻塞主线程,直到所有的比萨饼制造商都制作好了比萨饼。
public class CountDownLatchDemo {
public static void main(String[] args) {
int pizzaMakerCount = 4;
CountDownLatch countDownLatch = new CountDownLatch(pizzaMakerCount);
List<PizzaMaker> pizzaMakers = Stream.generate(() -> new PizzaMaker(3, countDownLatch))
.limit(pizzaMakerCount)
.collect(Collectors.toList());
pizzaMakers.stream()
.map(pizzaMaker -> new Thread(pizzaMaker))
.forEach(Thread::start);
try {
countDownLatch.await();
List<Pizza> allPizzas = new ArrayList<>();
pizzaMakers.forEach(pizzaMaker -> allPizzas.addAll(pizzaMaker.getPizzas()));
System.out.println(allPizzas.size() + " pizzas ready to go!");
} catch (InterruptedException e) {
System.out.println("Interrupted!");
}
}
}
该代码创建了四个pizza makers,并将相同的CountDownLatch传递给它们中的每一个。注意,CountDownLatch是用比萨饼制造商的数量实例化的。每一个负责制作三个比萨的比萨制作师都在一个单独的线程中运行。
然后主线程调用await()在……上CountDownLatch,因此要等到所有四个比萨饼制造商都调用了countDown()。一旦发生这种情况,主线程就开始将来自所有四个比萨饼制造商的比萨饼收集到一个集合中。
代码的输出如下所示:
Thread-2 - Pizza ready!
Thread-3 - Pizza ready!
Thread-0 - Pizza ready!
Thread-1 - Pizza ready!
Thread-1 - Pizza ready!
Thread-0 - Pizza ready!
Thread-2 - Pizza ready!
Thread-3 - Pizza ready!
Thread-3 - Pizza ready!
Thread-1 - Pizza ready!
Thread-2 - Pizza ready!
Thread-0 - Pizza ready!
12 pizzas ready to go!
我们可以看到最后的信息12 pizzas ready to go!在所有的比萨饼都由四个不同的比萨饼师傅做好之后显示。这就是如何使用CountDownLatch来阻塞一个线程,直到多个线程完成它们的任务。
场景2:基于先决条件等待开始的线程
在这个例子中,我们将拉伸pizza面团——对不起,场景——来说明如何使用CountDownLatch让线程池等待,直到满足某个条件。
在这个例子中,让我们考虑这样一个场景,许多比萨饼制作者正在等待他们的配料被取出,以便他们可以开始制作比萨饼。
为了实现这一点,每个比萨饼制作机将在一个单独的线程中运行,就像前面的例子一样。但是,这些线程需要等到满足条件(配料交付)后才能开始执行任务。
我们修改了PizzaMaker类来接收一个附加的CountDownLatch比萨饼制作者在制作比萨饼之前必须等待
class PizzaMaker implements FoodMaker, Runnable {
int numberOfPizzas;
List<Pizza> pizzas;
CountDownLatch ingredientsCountDownLatch;
CountDownLatch countDownLatch;
public PizzaMaker(int numberOfPizzas, CountDownLatch countDownLatch, CountDownLatch ingredientsCountDownLatch) {
this.numberOfPizzas = numberOfPizzas;
this.countDownLatch = countDownLatch;
this.ingredientsCountDownLatch = ingredientsCountDownLatch;
}
...
}
然后,我们修改make()方法来调用await()在……上ingredientsCountDownLatch因此每个比萨饼制作者在尝试制作比萨饼之前都要等待配料被取出:
@Override
public void make() {
try {
System.out.println(Thread.currentThread().getName() + " - Waiting for ingredients..");
ingredientsCountDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
pizzas = new ArrayList<>();
for(int i=0 ; i < numberOfPizzas; i++) {
try {
Pizza pizza = new Pizza();
Thread.sleep(1000);
pizza.setPrepared(true);
pizzas.add(pizza);
System.out.println(Thread.currentThread().getName() + " - Pizza ready!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
countDownLatch.countDown();
}
下面的演示代码显示了这种情况下的协调是如何完成的:
public class CountDownLatchDemo {
public static void main(String[] args) {
int pizzaMakerCount = 4;
CountDownLatch ingredientsCountDownLatch = new CountDownLatch(1);
CountDownLatch countDownLatch = new CountDownLatch(pizzaMakerCount);
List<PizzaMaker> pizzaMakers = Stream.generate(() -> new PizzaMaker(3, countDownLatch, ingredientsCountDownLatch))
.limit(pizzaMakerCount)
.collect(Collectors.toList());
pizzaMakers.stream()
.map(pizzaMaker -> new Thread(pizzaMaker))
.forEach(Thread::start);
try {
fetchIngredients();
System.out.println("Ingredients ready!");
ingredientsCountDownLatch.countDown();
countDownLatch.await();
List<Pizza> allPizzas = new ArrayList<>();
pizzaMakers.forEach(pizzaMaker -> allPizzas.addAll(pizzaMaker.getPizzas()));
System.out.println(allPizzas.size() + " pizzas ready to go!");
} catch (InterruptedException e) {
System.out.println("Interrupted!");
}
}
public static void fetchIngredients() throws InterruptedException {
Thread.sleep(5000);
}
}
请注意新的CountDownLatch —ingredientsCountDownLatch—初始化为计数1,因为只有主线程将获取配料。一旦fetchIngredients()方法完成后,主线程调用countDown()在ingredientsCountDownLatch,这导致每个PizzaMaker线程停止等待,并继续使比萨饼。
和以前一样,主线程调用await()在……上CountDownLatch与比萨饼制造商相关联,导致它一直等到所有比萨饼都准备好。
输出现在看起来像这样:
Thread-1 - Waiting for ingredients..
Thread-0 - Waiting for ingredients..
Thread-2 - Waiting for ingredients..
Thread-3 - Waiting for ingredients..
Ingredients ready!
Thread-2 - Pizza ready!
Thread-0 - Pizza ready!
Thread-3 - Pizza ready!
Thread-1 - Pizza ready!
Thread-0 - Pizza ready!
Thread-2 - Pizza ready!
Thread-3 - Pizza ready!
Thread-1 - Pizza ready!
Thread-0 - Pizza ready!
Thread-2 - Pizza ready!
Thread-3 - Pizza ready!
Thread-1 - Pizza ready!
12 pizzas ready to go!
我们可以看到,这四个比萨饼制造商在继续制作比萨饼之前,正在等待取来配料。最后的信息12 pizzas ready to go!在所有披萨都准备好之后显示。因此,我们在主线程和PizzaMaker线程。
值得注意的是,有一个变种的await()方法,该方法采用超时参数,以便调用线程可以避免无限期等待。
结论
我们已经看到了一个简单而有用的例子,展示了如何使用CountDownLatch以灵活的方式协调并发任务。
最后
机会是留给有准备的人,更多的java课程学习路线,笔记,面试等架构资料 想要学习的同学可以点击此链接添加备注暗号进行免费领取