1. 概述

在这篇文章中,我们将探讨如何检查一个包含多个Runnable对象的列表是否都已完成执行。我们知道,Runnable接口的实例可以作为Thread(/java-thread-lifecycle)运行。我们将使用如CompletableFutureThreadPoolExecutor这样的包装对象来运行这些线程。

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开始,我们可以使用内置的CompletableFutureisDone()方法来完成这个任务。

CompletableFuture使得Java中的异步编程更加简单。对于我们的Runnable列表,我们将使用CompletableFuturerunAsync()方法异步地执行关联的任务。请注意,这些任务默认会在ForkJoinPool上运行。

为了进一步操作,我们将所有结果的CompletableFuture封装到一个数组中:

CompletableFuture<?>[] completableFutures = runnables.stream()
    .map(CompletableFuture::runAsync)
    .toArray(CompletableFuture<?>[]::new);

现在,所有的Runnable任务都被包裹在CompletableFuture的执行中。这意味着这些任务将在后台异步运行,而我们的程序继续运行。

要在程序的任何时候检查所有执行是否已完成,我们将从数组创建一个新的包装CompletableFutureallOf()方法将帮助我们做到这一点。然后,我们将直接对包装的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上找到。