3 任务与线程
本章主要介绍任务的并发处理。
3.1 在线程中执行任务
串行执行任务
所有任务在单个线程中执行,程序简单,安全性也高。但无法提供高吞吐率和快速响应性。
显式为任务创建线程
通过创建新的线程为任务提供服务,将任务处理逻辑从主线程中分离出来,多个任务可以并行处理,提高了吞吐量和响应速度。
当短时间内要处理的任务较多时,为每一个任务创建一个新的线程并不是一个好的做法。因为线程的创建和销毁都是需要开销的,大量的线程在竞争CPU资源时还将产生其他的性能开销。另外,可创建的线程数量是有限制的,并不能无限创建。
3.2 Executor框架
任务是一组逻辑单元,而线程是使任务异步执行的机制。
Executor是异步任务处理框架的一个基础接口,线程池是它的一个重要实现部分。
Executor是基于生产者-消费者模式设计的,提交任务的操作相当于生产者,执行任务的线程则相当于消费者。通过将任务的提交与执行解耦开来,无须太大困难就可以为某种类型的任务指定和修改执行策略。
Executor只有一个方法,即execute方法,用于执行任务。ExecutorService是Executor的扩展接口,而线程池ThreadPoolExecutor即是ExecutorService的实现。博主以前的blog中已经介绍过线程池,这里不再赘述。
延迟任务与周期任务可通过Timer或ScheduledThreadPoolExecutor来实现。当然Timer功能较弱,完全可以用quartz代替。ScheduledThreadPoolExecutor只支持基于相对时间的调度。
3.3 找出可利用的并行性
当多个任务之间无依赖关系时,这些任务是可以并行处理的。
比如一段代码需要调用三个外部系统,获得全部调用返回结果之后再进行其他处理,如果串行执行的话所花费的时间就是三次调用之和,而并行处理的话那么就只需要花费耗时最久的那个调用时间。
并行处理的任务需要能返回结果。Executor框架使用Runnable作为其基本的任务表示形式,但Runnable不能返回一个值或抛出一个受检查的异常。
3.3.1 Callable与Future、FutureTask
在上一篇的同步工具类里介绍FutureTask时提到过Callable和Future。
与Runnable相比,Callable的接口实例可以实现有返回值的线程任务,其返回值就放在Future中。
Future表示一个任务的生命周期,提供了相应的方法来判断任务是否已经完成或取消,以及获取任务的结果和取消任务等。
在Future中,get方法用于获取任务结果,其行为取决于任务的状态(未开始、正在运行、已完成)。如果任务正常执行完成,那么get会立即返回结果;如果任务没有完成,那么get将阻塞并直到任务完成。
当任务执行过程中抛出异常时,get会将该异常封装为ExecutionException并重新抛出,通过getCause可以获取初始异常。当任务取消时,get会抛出CancellationException。当任务中断时抛出InterruptedException。因此通过get的返回值,可以判断任务的状态。
FutureTask是一个异步任务类,实现了RunnableFuture接口,而Runnable接口继承了Runnable接口和Future接口。因此FutureTask既可以作为任务提交,又能返回任务执行的结果。当需要返回任务执行结果时,需要对Callable对象进行包装,不需要返回任务结果时则可以对Runnable对象进行包装。
3.3.2 CompletionService
如果向Executor提交了一组计算任务,并且在计算完成后获得结果,那么可以保留与每个任务关联的Future,然后通过轮询的方式来判断任务是否完成。
这种实现在上一篇中已经举过栗子,通过两个while循环来轮询遍历,按执行顺序先后输出最终结果。随后提出通过FutureTask来对Callable对象进行包装,然后覆写FutureTask的done方法来实现任务完成的回调,这样就不用轮询遍历了。
事实上还有另一种方式,即通过ComletionService来实现:
public static void main(String[] args) throws Exception {
// 通过5个线程执行任务,并输出最终结果
ExecutorService executorService = Executors.newFixedThreadPool(5);
CompletionService<String> completionService = new ExecutorCompletionService(executorService);
for (int i = 0; i < 5; i++) {
completionService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
TimeUnit.SECONDS.sleep(new Random().nextInt(2));
return Thread.currentThread().getName();
}
});
}
for (int i = 0; i < 5; i++) {
Future<String> future = completionService.take();
try {
System.out.println(future.get());
} catch (ExecutionException ee) {
logger.error("任务执行异常,{}", ee.getCause());
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
executorService.shutdown();
}
通过CompletionService提交任务,CompletionService会将任务委托给ExecutorService来执行,并根据任务执行完成的顺序自动将结果放到BlockingQueue中。开发者只需要通过take方法取出任务Future获得顺序执行的结果。
ExecutorCompltionService的实现是相当简单的,就是在构造函数中创建一个BlockingQueue来保存任务结果。当任务执行完成时,调用FutureTask中的done方法,将结果放入BlockingQueue中。
此种方式不需要轮询遍历,也不需要覆写FutureTask的任务完成回调方法。事实上其内部实现就是结果保存和任务完成回调的结合。
3.4 任务取消与关闭
待续。