1. 概述
在这篇文章中,我们将探讨如何检查一个包含多个Runnable
对象的列表是否都已完成执行。我们知道,Runnable
接口的实例可以作为Thread
(/java-thread-lifecycle)运行。我们将使用如CompletableFuture
和ThreadPoolExecutor
这样的包装对象来运行这些线程。
2. 示例设置
首先,我们创建一个简单的Runnable
,它只会记录一条消息,然后暂停一毫秒:
static Runnable RUNNABLE = () -> {
try {
System.out.println("launching runnable");
Thread.sleep(1000);
} catch (InterruptedException e) {
}
};
现在,我们将创建一个List<Runnable>
。在这个例子中,我们将重复添加相同的Runnable
。一种实现方式是使用IntStream
:
List<Runnable> runnables = IntStream.range(0, 5)
.mapToObj(x -> RUNNABLE)
.collect(Collectors.toList());
接下来,我们将展示如何运行这些Runnable
对象,并了解它们是否都已完成。
3. 使用CompletableFuture
从Java 8开始,我们可以使用内置的CompletableFuture
的isDone()
方法来完成这个任务。
CompletableFuture
使得Java中的异步编程更加简单。对于我们的Runnable
列表,我们将使用CompletableFuture
的runAsync()
方法异步地执行关联的任务。请注意,这些任务默认会在ForkJoinPool
上运行。
为了进一步操作,我们将所有结果的CompletableFuture
封装到一个数组中:
CompletableFuture<?>[] completableFutures = runnables.stream()
.map(CompletableFuture::runAsync)
.toArray(CompletableFuture<?>[]::new);
现在,所有的Runnable
任务都被包裹在CompletableFuture
的执行中。这意味着这些任务将在后台异步运行,而我们的程序继续运行。
要在程序的任何时候检查所有执行是否已完成,我们将从数组创建一个新的包装CompletableFuture
。allOf()
方法将帮助我们做到这一点。然后,我们将直接对包装的CompletableFuture
应用isDone()
方法:
boolean isEveryRunnableDone = CompletableFuture.allOf(completableFutures)
.isDone();
如果任何CompletableFuture
仍在运行,isEveryRunnableDone
将为false
,否则为true
。
4. 使用ThreadPoolExecutor
自Java 5以来,线程池提供了额外的工具,以帮助并发环境中的资源管理。特别是,它们维护了一些统计信息,如它们持有的已完成任务的数量。
4.1. 统计剩余任务数量
让我们创建一个带有五个线程的ThreadPoolExecutor
。然后,我们将使用execute()
方法提交每个Runnable
进行执行:
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(5);
runnables.forEach(executor::execute);
现在,我们可以使用getActiveCount()
方法来统计线程池中正在运行的任务数量:
int numberOfActiveThreads = executor.getActiveCount();
关键问题是,我们能否仅仅通过比较这个数字与0来判断是否有Runnable
仍在运行?实际上,事情比这复杂一些。问题在于,getActiveCount()
方法返回的数值是一个近似值,正如类文档所述,因此我们不能依赖它来做决策。
4.2. 检查所有任务是否已终止
getActiveCount()
方法不返回精确的值,因为这样做可能会相当计算密集。因此,我们无法自己实现计数器。
另一方面,awaitTermination()
方法会告诉我们所有任务是否已完成。 首先,我们需要调用执行器的shutdown()
方法。此方法确保提交的所有任务都将完成。然而,它阻止了新任务添加到执行器:
executor.shutdown();
我们已经确保ThreadPoolExecutor
将正确关闭。现在,我们可以在任何时候通过调用awaitTermination()
来检查池中是否有正在运行的任务。这个方法将在给定的超时或所有任务完成后阻塞。例如,为了示例,我们使用一秒的超时时间:
boolean isEveryRunnableDome = executor.awaitTermination(1000, TimeUnit.MILLISECONDS);
如果所有任务在一秒钟内完成,方法立即返回true
。否则,程序将在一秒钟后被阻塞并返回false
。
最后但同样重要的是,我们应该注意,如果底层线程之一被中断,awaitTermination()
将抛出一个InterruptedException
。
5. 总结
在这篇教程中,我们了解了如何检查所有Runnable
是否都已完成。对于Java 8以上的版本,这得益于CompletableFuture
类,非常直观。对于较旧的版本,我们需要明智地选择超时时间,因为程序可能会被阻塞我们设置的时间长度。
如往常一样,代码可以在GitHub上找到。