ExecutorService使用指南-Java快速入门教程
1. 概述
ExecutorService是一个 JDK API,可简化在异步模式下运行任务的过程。一般来说,ExecutorService会自动提供一个线程池和一个用于为其分配任务的API。
2. 实例化执行器服务
2.1.执行器类的工厂方法
创建ExecutorService的最简单方法是使用Executors类的工厂方法之一。
例如,以下代码行将创建一个包含 10 个线程的线程池:
ExecutorService executor = Executors.newFixedThreadPool(10);
还有其他几种工厂方法可以创建满足特定用例的预定义执行器服务。要找到满足您需求的最佳方法,请参阅Oracle 的官方文档。
2.2. 直接创建ExecutorService
由于ExecutorService是一个接口,因此可以使用其任何实现的实例。java.util.concurrent包中有多种实现可供选择,或者您可以创建自己的实现。
例如,ThreadPoolExecutor类有几个构造函数,我们可以用来配置执行器服务及其内部池:
ExecutorService executorService =
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
3. 将任务分配给ExecutorService
ExecutorService可以执行可运行和可调用的任务。为了简单起见,本文将使用两个简单任务。请注意,我们在这里使用 lambda 表达式而不是匿名内部类:
Runnable runnableTask = () -> {
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Callable<String> callableTask = () -> {
TimeUnit.MILLISECONDS.sleep(300);
return "Task's execution";
};
List<Callable<String>> callableTasks = new ArrayList<>();
callableTasks.add(callableTask);
我们可以使用多种方法将任务分配给ExecutorService,包括从Executor接口继承的execute(),以及 submit()、invokeAny() 和 invokeAll()。
execute() 方法没有返回结果,因此无法获取任务执行的结果或检查任务的状态(是否正在运行):
executorService.execute(runnableTask);
submit() 将可调用或可运行的任务提交给ExecutorService,并返回Future 类型的结果:
Future<String> future =
executorService.submit(callableTask);
invokeAny() 将一组任务分配给ExecutorService,使每个任务运行,并返回成功执行一个任务的结果(如果成功执行):
String result = executorService.invokeAny(callableTasks);
invokeAll() 将一组任务分配给ExecutorService,然后每个任务都得以运行,并以Future 类型的对象列表的形式返回所有任务执行的结果:
List<Future<String>> futures = executorService.invokeAll(callableTasks);
在继续之前,我们需要讨论另外两个项目:关闭执行器服务和处理Future返回类型。
4. 关闭ExecutorService
一般来说,当没有要处理的任务时,执行器服务不会被自动销毁。它将保持活力并等待新的工作完成。
在某些情况下,这非常有用,例如当应用需要处理不规则出现的任务或在编译时任务数量未知时。
另一方面,应用程序可以到达其终点,但不会停止,因为等待的执行器服务将导致 JVM 继续运行。
要正确关闭ExecutorService,我们有 shutdown() 和shutdownNow()API。
shutdown() 方法不会导致立即销毁ExecutorService。它将使执行器服务停止接受新任务,并在所有正在运行的线程完成其当前工作后关闭:
executorService.shutdown();
shutdownNow() 方法试图立即销毁ExecutorService,但它不能保证所有正在运行的线程将同时停止:
List<Runnable> notExecutedTasks = executorService.shutDownNow();
此方法返回等待处理的任务的列表。由开发人员决定如何处理这些任务。
关闭ExecutorService(也是Oracle 推荐的)的一个好方法是将这两种方法与awaitTermination() 方法结合使用:
executorService.shutdown();
try {
if (!executorService.awaitTermination(800, TimeUnit.MILLISECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
}
使用这种方法,执行器服务将首先停止接受新任务,然后等待指定的时间段以完成所有任务。如果该时间到期,则立即停止执行。
5.Future接口
submit() 和invokeAll() 方法返回一个对象或Future 类型的对象集合,这允许我们获取任务执行的结果或检查任务的状态(它是否正在运行)。
Future接口提供了一个特殊的阻塞方法get(),它返回Callable任务执行的实际结果,如果是Runnable任务,则返回null:
Future<String> future = executorService.submit(callableTask);
String result = null;
try {
result = future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
在任务仍在运行时调用get() 方法将导致执行阻塞,直到任务正确执行并且结果可用。
由于get() 方法导致的长时间阻塞,应用程序的性能可能会降低。如果生成的数据不重要,则可以通过使用超时来避免此类问题:
String result = future.get(200, TimeUnit.MILLISECONDS);
如果执行周期长于指定时间(在本例中为 200 毫秒),则会引发超时异常。
我们可以使用isDone() 方法来检查分配的任务是否已处理。
Future接口还提供了使用cancel() 方法取消任务执行和使用isCancel() 方法检查取消:
boolean canceled = future.cancel(true);
boolean isCancelled = future.isCancelled();
6.ScheduledExecutorService接口
ScheduledExecutorService在预定义的延迟和/或定期运行任务。
同样,实例化ScheduledExecutorService的最佳方法是使用Executors类的工厂方法。
在本节中,我们使用带有一个线程的ScheduledExecutorService:
ScheduledExecutorService executorService = Executors
.newSingleThreadScheduledExecutor();
若要在固定延迟后安排单个任务的执行,请使用ScheduledExecutorService 的scheduled() 方法。
两个scheduled() 方法允许您执行Runnable或Callable任务:
Future<String> resultFuture =
executorService.schedule(callableTask, 1, TimeUnit.SECONDS);
scheduleAtFixedRate()方法允许我们在固定延迟后定期运行任务。上面的代码在执行可调用任务之前延迟了一秒钟。
以下代码块将在初始延迟 100 毫秒后运行任务。之后,它将每 450 毫秒运行一次相同的任务:
executorService.scheduleAtFixedRate(runnableTask, 100, 450, TimeUnit.MILLISECONDS);
如果处理器运行分配的任务所需的时间比scheduleAtFixedRate() 方法的period参数多,则ScheduledExecutorService将等到当前任务完成后再开始下一个任务。
如果需要在任务迭代之间有固定的长度延迟,则应使用 scheduleWithFixedDelay()。
例如,以下代码将保证在当前执行结束和另一个执行开始之间有 150 毫秒的暂停:
executorService.scheduleWithFixedDelay(task, 100, 150, TimeUnit.MILLISECONDS);
根据scheduleAtFixedRate() 和scheduleWithFixedDelay() 方法合约,任务的周期执行将在ExecutorService终止或在任务执行期间引发异常时结束。
7.ExecutorService与fork/join
Java 7 发布后,许多开发人员决定用 fork/join 框架替换ExecutorService框架。
然而,这并不总是正确的决定。尽管与分叉/联接相关的简单性和频繁的性能提升,但它降低了开发人员对并发执行的控制。
ExecutorService使开发人员能够控制生成的线程数以及应由单独线程运行的任务的粒度。ExecutorService的最佳用例是处理独立任务,例如根据“一个线程对应一个任务”的方案处理事务或请求。
相比之下,根据 Oracle 的文档,fork/join 旨在加快可以递归分解为更小部分的工作。
8. 结论
尽管ExecutorService相对简单,但仍有一些常见的陷阱。
让我们总结一下:
使未使用的执行程序服务保持活动状态:请参阅第 4 节中有关如何关闭执行程序服务的详细说明。
使用固定长度线程池时线程池容量错误:确定应用程序需要多少线程才能高效运行任务非常重要。太大的线程池将导致不必要的开销,只是为了创建主要处于等待模式的线程。太少会使应用程序看起来无响应,因为队列中的任务等待时间很长。
在任务取消后调用Future 的get() 方法:尝试获取已取消任务的结果会触发CancelException。
使用 Future 的get() 方法意外长时间阻塞:我们应该使用超时来避免意外的等待。