Spring Boot教程(21) – 默认线程池

之前我们简要说过@Async@Scheduled的用法,这俩注解会帮你完成异步任务和定时任务的需求。不知道你有没有想过,这些异步任务和定时任务都是在哪个线程执行的?Spring Boot肯定在背后做了很多工作,本文就来说说框架都为我们做了什么。

首先肯定是有线程池的。Spring Boot已经帮你创建并配置好了,还配了两个,一个供@Async使用,一个供@Scheduled使用。

Spring将异步任务和定时任务的执行,抽象出了两个接口,TaskExecutorTaskScheduler。我们先来说说TaskExecutor

如果你对Java的线程池相关的API比较熟,那么在需要使用线程池的场景,你可能会用Executors来生成ExecutorService(继承于Executor),从而执行任务。Spring中的TaskExecutor其实和Java的Executor是一样的,只不过后者是在Java 5的时候被引入的,Spring为了兼容之前的Java版本,自己搞了一套。时至今日,Spring已经最低要求Java 8了,但是为了兼容之前的Spring版本,还是保持了自己的API,基础的TaskExecutor接口已经直接继承于Executor了(如上图)。

TaskExecutor有很多实现,比如SyncTaskExecutor会在调用者的当前线程同步执行任务;比如SimpleAsyncTaskExecutor会针对每个任务新建一个线程,运行完线程就停止;比如你可以通过ConcurrentTaskExecutor,将Executor对象包装成TaskExecutor对象,这样将Java的Executor对象纳入到Spring管理,方便使用;再比如最常用的ThreadPoolTaskExecutor,内部有个线程池,任务扔进去运行就完了。实际上ThreadPoolTaskExecutor封装了Java的ThreadPoolExecutor,封装的同时也会对其进行一些配置。

Spring Boot会帮你自动生成一个ThreadPoolTaskExecutor,进行了默认配置,相关代码在TaskExecutionAutoConfiguration。使用如下属性,还可以对线程池进行自定义,比如核心线程数目、最多线程数目、线程名前缀等等:

如果你的代码里需要ThreadPoolTaskExecutor,直接通过@Autowired引入就行了。而@Async注解用的正是Spring Boot自动生成的这个对象,具体一点说,就是它会去容器里找TaskExecutor类型的Bean,如果有多个,他会再去找名为“taskExecutor”、类型为Executor的Bean。我在源码里扒拉半天才搞清楚的,如果也想搞清楚,可以查看AsyncExecutionAspectSupportgetDefaultExecutor方法。

下面再来说说TaskScheduler

TaskScheduler倒是没有什么历史遗留问题,你看看上图中的它的方法,会发现,这不跟@Scheduled注解的参数是大致对应的嘛,简直太好理解了:

我们上面说了,定时任务也有一个对应的线程池,具体实现由ThreadPoolTaskScheduler负责的,它其实是封装了Java里的ScheduledExecutorService。并且也有对应的属性方便你去自定义:

如果你研究透了之后会发现,以上其实就是典型的Spring Boot的特点,“自动配置好,不够你自己改”。你通过@EnableAsync@EnableScheduling来开启功能,然后就可以通过@Async@Scheduled来执行异步任务和定时任务,这些任务会分别扔给容器里的ThreadPoolTaskExecutorThreadPoolTaskScheduler,然后放到各自的线程池中运行,想要自定义可以在application.properties中修改属性,如果还觉得不够用,甚至可以给容器提供自己的TaskExecutorTaskScheduler实现来覆盖Spring Boot默认给你的对象。如果你只想自定义@Async使用的线程池,可以通过AsyncConfigurer提供一个自己的Executor实现。这样一步步由浅入深,由简单场景过渡到复杂场景,各种需求都可以满足。如果有人还不理解Spring Boot的特点到底是啥,你可以拿本文的例子来讲。

再说说@Async的异常处理。如果你的@Async方法的返回值是Future类型或者ListenableFuture或者CompletableFuture等类型,那么你在调用Future.get()方法的时候,异常会被抛出。如果你的@Async方法的返回值是void,那么你可以通过AsyncConfigurer传递一个全局的AsyncUncaughtExceptionHandler对象用来处理异常,它会跟你的任务在同一线程执行。

还有几个类,Spring文档并没有给出怎么用,但是源码告诉了大家怎么用。

比如TaskExecutorCustomizer,你的ThreadPoolTaskExecutor在创建完成并通过application.properties做了定制之后,还可以通过TaskExecutorCustomizer进行进一步的定制。对应的,TaskSchedulerCustomizer还可以对ThreadPoolTaskScheduler进行定制。比如TaskDecorator,可以对放到ThreadPoolTaskExecutor里执行的Runnable进行封装。你需要做的就是实现TaskExecutorCustomizerTaskSchedulerCustomizer或者TaskDecorator,并通过@Bean方法或者@Component注解,将他们放到容器中去。

PS:我写完之后反复读了本文,感觉如果不跟着源码一起看的话,很可能不能很好理解。源码和概念相结合,使用效果更佳。

发表评论

电子邮件地址不会被公开。