20.4 任务执行服务

20.4 任务执行服务

关于任务执行服务,我们介绍了:

  • 任务执行服务的基本概念。
  • 主要实现方式:线程池。
  • 定时任务。

(1)基本概念

任务执行服务大大简化了执行异步任务所需的开发,它引入了一个“执行服务”的概念,将“任务的提交”和“任务的执行”相分离,“执行服务”封装了任务执行的细节,对于任务提交者而言,它可以关注于任务本身,如提交任务、获取结果、取消任务,而不需要关注任务执行的细节,如线程创建、任务调度、线程关闭等。

任务执行服务主要涉及以下接口:

  • Runnable和Callable:表示要执行的异步任务。
  • Executor和ExecutorService:表示执行服务。
  • Future:表示异步任务的结果。

使用者只需要通过ExecutorService提交任务,通过Future操作任务和结果即可,不需要关注线程创建和协调的细节。

(2)线程池

任务执行服务的主要实现机制是线程池,实现类是ThreadPoolExecutor。线程池主要由两个概念组成:一个是任务队列;另一个是工作者线程。任务队列是一个阻塞队列,保存待执行的任务。工作者线程主体就是一个循环,循环从队列中接收任务并执行。ThreadPool-Executor有一些重要的参数,理解这些参数对于合理使用线程池非常重要,18.2节对这些参数进行了详细介绍。

ThreadPoolExecutor实现了生产者/消费者模式,工作者线程就是消费者,任务提交者就是生产者,线程池自己维护任务队列。当我们碰到类似生产者/消费者问题时,应该优先考虑直接使用线程池,而非“重新发明轮子”,自己管理和维护消费者线程及任务队列。

(3)定时任务

异步任务中,常见的任务是定时任务。在Java中,有两种方式实现定时任务:
1)使用java.util包中的Timer和TimerTask。
2)使用Java并发包中的ScheduledExecutorService。

Timer有一些需要特别注意的事项:
1)一个Timer对象背后只有一个Timer线程,这意味着,定时任务不能耗时太长,更不能是无限循环。
2)在执行任何一个任务的run方法时,一旦run抛出异常,Timer线程就会退出,从而所有定时任务都会被取消。

ScheduledExecutorService的主要实现类是ScheduledThreadPoolExecutor,它没有Timer的问题。
1)它的背后是线程池,可以有多个线程执行任务。
2)任务执行线程会捕获任务执行过程中的所有异常,一个定时任务的异常不会影响其他定时任务。

所以,实践中建议使用ScheduledExecutorService。

针对多线程开发的两个核心问题:竞争和协作,本章总结了线程安全和协作的多种机制,针对高层服务,本章总结了并发容器和任务执行服务,它们让我们在更高的层次上访问共享的数据结构,执行任务,而避免陷入线程管理的细节。

有一些并发的内容,我们没有讨论,比如以下内容。
1)Java 7引入的Fork/Join框架,Java 8中有并行流的概念,可以让开发者非常方便地对大量数据进行并行操作,背后基于的就是Fork/Join框架,关于流我们在第26章会进一步介绍。
2)CompletionService,在异步任务程序中,一种场景是:主线程提交多个异步任务,然后希望有任务完成就处理结果,并且按任务完成顺序逐个处理,对于这种场景,Java并发包提供了一个方便的方法,那就是使用CompletionService。这是一个接口,它的实现类是ExecutorCompletionService,它通过一个额外的结果队列,方便了对于多个异步任务结果的处理,细节可参考微信公众号“老马说编程”第79篇文章。
3)Java 8引入组合式异步编程CompletableFuture,它可以方便地将多个有一定依赖关系的异步任务以流水线的方式组合在一起,自然地表达任务之间的依赖关系和执行流程,大大简化代码,提高可读性。关于CompletableFuture,我们也到第26章介绍。

从下一章开始,我们来探讨Java中的一些动态特性,比如反射、注解、动态代理等,它们到底是什么呢?