Future 指南-Java快速入门教程
1. 概述
在本教程中,我们将了解Future 。自Java 1.5以来一直存在的接口,在处理异步调用和并发处理时非常有用。
2. 创建Future实例
简单地说,Future类表示异步计算的未来结果。此结果最终将在处理完成后显示在将来。
让我们看看如何编写创建和返回Future实例的方法。
长时间运行的方法非常适合异步处理和 Future 接口,因为我们可以在等待Future中封装的任务完成时执行其他进程。
利用Future异步特性的一些操作示例包括:
- 计算密集型过程(数学和科学计算)
- 操作大数据结构(大数据)
- 远程方法调用(下载文件、HTML 报废、Web 服务)
2.1. 使用FutureTask来产生Future实例
对于我们的示例,我们将创建一个非常简单的类来计算整数的平方。这绝对不适合长时间运行的方法类别,但我们将对它进行Thread.sleep() 调用,以便它在完成之前持续 1 秒:
public class SquareCalculator {
private ExecutorService executor
= Executors.newSingleThreadExecutor();
public Future<Integer> calculate(Integer input) {
return executor.submit(() -> {
Thread.sleep(1000);
return input * input;
});
}
}
实际执行计算的代码位包含在call() 方法中,并作为 lambda 表达式提供。正如我们所看到的,除了前面提到的sleep()调用之外,它没有什么特别之处。
当我们把注意力集中在Callable和ExecutorService的使用上时,它会变得更加有趣。
Callable是一个接口,表示返回结果的任务,并且具有单个call() 方法。在这里,我们使用 lambda 表达式创建了一个实例。
创建Callable实例不会带我们去任何地方;我们仍然必须将此实例传递给执行器,该执行器将负责在新线程中启动任务,并将有价值的Future对象返回给我们。这就是ExecutorService的用武之地。
有几种方法可以访问ExecutorService实例,其中大多数都是由实用程序类Executors 的静态工厂方法提供的。在这个例子中,我们使用了基本的newSingleThreadExecutor(),它为我们提供了一个能够一次处理单个线程的ExecutorService。
一旦我们有一个ExecutorService对象,我们只需要调用submit(),将我们的 Callable作为参数传递。然后submit() 将启动任务并返回一个FutureTask对象,它是Future接口的实现。
3. 使用Futures
到目前为止,我们已经学会了如何创建Future 的实例。
在本节中,我们将通过探索Future API 中的所有方法来学习如何使用此实例。
3.1. 使用isDone() 和get() 获取结果
现在我们需要调用calculate(),并使用返回的Future来获取结果的整数。未来API 中的两种方法将帮助我们完成此任务。
Future.isDone()告诉我们执行者是否完成了任务的处理。如果任务完成,它将返回true;否则,它将返回false。
返回计算实际结果的方法是Future.get()。我们可以看到,此方法阻塞当前线程的执行,直到任务完成。但是,在我们的示例中,这不会成为问题,因为我们将通过调用isDone() 来检查任务是否完成。
通过使用这两种方法,我们可以在等待主任务完成的同时运行其他代码:
Future<Integer> future = new SquareCalculator().calculate(10);
while(!future.isDone()) {
System.out.println("Calculating...");
Thread.sleep(300);
}
Integer result = future.get();
在此示例中,我们将在输出上编写一条简单的消息,让用户知道程序正在执行计算。
方法get()将阻塞当前线程的执行,直到任务完成。同样,这不会成为问题,因为在我们的示例中,get() 只会在确保任务完成后调用。所以在这种情况下,future.get() 将始终立即返回。
值得一提的是,get()有一个重载版本,它采用超时和TimeUnit作为参数:
Integer result = future.get(500, TimeUnit.MILLISECONDS);
get(long, TimeUnit) 和get() 之间的区别在于,如果任务在指定的超时期限之前未返回,前者将抛出TimeoutException。
3.2. 使用cancel() 取消Future
假设我们触发了一个任务,但由于某种原因,我们不再关心结果。我们可以使用Future.cancel(布尔值)告诉执行器停止操作并中断其底层线程:
Future<Integer> future = new SquareCalculator().calculate(4);
boolean canceled = future.cancel(true);
我们的 Future 实例,从上面的代码来看,永远不会完成它的操作。事实上,如果我们尝试从该实例调用get(),在调用cancel() 之后,结果将是一个CancelException。Future.isCancel()将告诉我们future是否已经被取消。这对于避免出现取消异常非常有用。
对cancel() 的调用也可能失败。在这种情况下,返回的值将为false。需要注意的是,cancel() 将布尔值作为参数。这控制是否应中断执行任务的线程。
4. 使用线程池进行更多多线程处理
我们当前的ExecutorService是单线程的,因为它是使用Executors.newSingleThreadExecutor 获得的。为了突出显示这个单线程,让我们同时触发两个计算:
SquareCalculator squareCalculator = new SquareCalculator();
Future<Integer> future1 = squareCalculator.calculate(10);
Future<Integer> future2 = squareCalculator.calculate(100);
while (!(future1.isDone() && future2.isDone())) {
System.out.println(
String.format(
"future1 is %s and future2 is %s",
future1.isDone() ? "done" : "not done",
future2.isDone() ? "done" : "not done"
)
);
Thread.sleep(300);
}
Integer result1 = future1.get();
Integer result2 = future2.get();
System.out.println(result1 + " and " + result2);
squareCalculator.shutdown();
现在让我们分析此代码的输出:
calculating square for: 10
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
calculating square for: 100
future1 is done and future2 is not done
future1 is done and future2 is not done
future1 is done and future2 is not done
100 and 10000
很明显,这个过程不是平行的。我们可以看到,第二个任务仅在第一个任务完成后开始,使整个过程大约需要 2 秒才能完成。
为了使我们的程序真正多线程,我们应该使用不同风格的ExecutorService。让我们看看如果我们使用工厂方法Executors.newFixedThreadPool() 提供的线程池,示例的行为会如何变化:
public class SquareCalculator {
private ExecutorService executor = Executors.newFixedThreadPool(2);
//...
}
通过对SquareCalculator类进行简单的更改,我们现在有一个能够同时使用 2 个线程的执行器。
如果我们再次运行完全相同的客户端代码,我们将得到以下输出:
calculating square for: 10
calculating square for: 100
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
100 and 10000
现在看起来好多了。我们可以看到,这两个任务同时开始和结束运行,整个过程大约需要 1 秒才能完成。
还有其他工厂方法可用于创建线程池,例如 Executors.newCachedThreadPool(),它在以前使用的Thread可用时重用它们,以及Executors.newScheduledThreadPool(),它调度命令在给定延迟后运行。
有关ExecutorService的更多信息,请阅读我们专门针对该主题的文章。
5.ForkJoinTask概述
ForkJoinTask是一个实现Future的抽象类,能够运行ForkJoinPool中少量实际线程托管的大量任务。
在本节中,我们将快速介绍ForkJoinPool 的主要特征。有关该主题的综合指南,请查看我们的Java 中的 Fork/Join 框架指南。
ForkJoinTask的主要特征是它通常会生成新的子任务,作为完成其主要任务所需工作的一部分。它通过调用fork()生成新任务,并使用join()收集所有结果,因此类的名称。
有两个抽象类实现了ForkJoinTask:RecursiveTask,它在完成时返回一个值,以及RecursiveAction,它不返回任何内容。顾名思义,这些类将用于递归任务,例如文件系统导航或复杂的数学计算。
让我们扩展前面的示例,创建一个类,给定一个Integer,该类将计算其所有阶乘元素的总和平方。因此,例如,如果我们将数字 4 传递给我们的计算器,我们应该从 4² + 3² + 2² + 1² 的总和中得到结果,即 30。
首先,我们需要创建一个具体的递归任务实现并实现它的compute() 方法。我们将在这里编写业务逻辑:
public class FactorialSquareCalculator extends RecursiveTask<Integer> {
private Integer n;
public FactorialSquareCalculator(Integer n) {
this.n = n;
}
@Override
protected Integer compute() {
if (n <= 1) {
return n;
}
FactorialSquareCalculator calculator
= new FactorialSquareCalculator(n - 1);
calculator.fork();
return n * n + calculator.join();
}
}
注意我们如何通过在compute() 中创建FactorialSquareCalculator的新实例来实现递归性。通过调用fork(),一种非阻塞方法,我们要求ForkJoinPool启动此子任务的执行。
join() 方法将返回该计算的结果,我们将在其中添加当前访问的数字的平方。
现在我们只需要创建一个ForkJoinPool来处理执行和线程管理:
ForkJoinPool forkJoinPool = new ForkJoinPool();
FactorialSquareCalculator calculator = new FactorialSquareCalculator(10);
forkJoinPool.execute(calculator);
6. 结论
在本文中,我们全面探讨了Future接口,涉及它的所有方法。我们还学习了如何利用线程池的强大功能来触发多个并行操作。ForkJoinTask类 fork() 和join() 中的主要方法也被简要介绍。
我还有许多其他关于 Java 中的并行和异步操作的精彩文章。以下是其中三个与Future界面密切相关的界面,其中一些已经在文章中提到过:
- CompletableFuture指南——Future的实现,在Java 8中引入了许多额外的功能
- Java 中的 Fork/Join 框架指南 – 更多关于我们在第 5 节中介绍的ForkJoinTask的信息
- Java ExecutorService指南——专用于ExecutorService接口