如果我们希望某段代码异步执行的话,通常是创建一个继承或实现线程相关的类或接口来实现的,具体过程不在本文赘述。但是如果我们所有的代码都是创建线程去执行的话,就会遇到线程数不可控等诸多问题,于是我们引入了线程池的概念,相关讨论可参考《十个为什么》之四:为什么要有线程池?。
即使有了线程池,对于异步调用的代码我们还是需要每次都为它创建线程类,这个麻烦事在 Java8 中通过 lambda 表达式可以省略重复代码。其实,对于这样的需求,Spring 也提供了解决方案:@EnableAsync 和 @Async 注解,其中,@EnableAsyn 注解写在启动类上就行了,它表示本项目启用了异步调用的功能。如下图所示:
这样,就再也不需要自己手动创建线程池来实现异步调用了,而异步的方法也不需要再主动实现Runnable之类的接口了,而只需要把 @Async 注解写在你希望异步调用的方法上就行,如下图所示:
凡是加上了 @Async 注解的方法,被调用后它都是异步执行的。
1. TaskExecutor
这种方式确实非常方便,但是我们还是需要注意一下线程池的问题,因为到目前为止,被 @Async 注解的方法,每次执行都会创建一个线程去执行的。Spring 需要我们去配置一个叫做 TaskExecutor 的东西。
默认情况下,Spring 将搜索相关的线程池定义:要么在上下文中搜索唯一的 TaskExecutor bean,要么搜索名为 "taskExecutor" 的 Executor bean。如果两者都无法解析,则将使用 SimpleAsyncTaskExecutor 来处理异步方法调用。
通常,应该定义一个 ThreadPoolTaskExecutor 来使用线程池,使得异步调用的多线程是可控的。
Spring 已经实现的异步线程池有以下5个:
SimpleAsyncTaskExecutor
作用:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程
适用性:默认值、不推荐;这是没有配置 TaskExecutor 时,Spring 的默认选择
SyncTaskExecutor
作用: 这个类没有实现异步调用,任务是同步进行的;
适用性:不需要多线程的地方
ConcurrentTaskExecutor
作用:Executor 的适配类
适用性:不推荐;ThreadPoolTaskExecutor 不满足要求才用考虑使用这个类
SimpleThreadPoolTaskExecutor
作用:是 Quartz 的 SimpleThreadPool 的类;
适用性:线程池同时被 quartz 和非 quartz 使用,才需要使用此类
ThreadPoolTaskExecutor
作用:实质上是对 java.util.concurrent.ThreadPoolExecutor 的封装
适用性:最常使用,推荐
此外,我在《十个为什么》之四:为什么要有线程池?一文中讨论了线程池的作用以及相关参数的意义,其中还讨论了一个问题:一个项目中是否需要使用统一的线程池去管理所有的多线程操作呢?我认为没有这个必要,因为 Java 中使用线程池声明的线程,在没有实际使用的时候,是几乎不会消耗系统资源的。基于上述原因,我们可以针对不同的场景创建不同的线程池。但如果项目中都使用了上述做法,那么等于只有一个公用的线程池了——或许使用同一个线程池并不是什么问题,只要我们为此设计一个更加通用的线程池即可。
2. @Async
注解 @Async 下的方法是一个异步执行的方法
异步的方法有3种
- 最简单的异步调用,返回值为void
- 带参数的异步调用 异步方法可以传入参数
- 异常调用返回Future
如下方式会使 @Async 失效
-
异步方法使用 static 修饰,失效
-
异步类没有使用 @Component 注解(或其他注解)导致 Spring 无法扫描到异步类,失效
-
类的对象需要使用 @Autowired 或 @Resource 等注解自动注入,不能自己手动 new 对象,否则失效
-
如果使用 SpringBoot 框架必须在启动类中增加 @EnableAsync 注解,或者把该注解放在某个配置类中,否则失效
-
在 Async 方法上标注 @Transactional 是没用的。 在 Async 方法调用的方法上标注 @Transactional 有效,但该事务注解不会控制到异步方法中的代码
-
调用被 @Async 标记的方法的调用者不能和被调用的方法在同一类中不然不会起作用
3. 开启异步的配置类
下面是一个配置的代码 demo:
@Configuration
@EnableAsync // 启动异步调用
public class ThreadPoolTaskConfig {
@Bean("taskExecutor") // bean的名称,默认为首字母小写的方法名
public ThreadPoolTaskExecutor getAsyncExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("Async-Service-"); // 线程池名前缀
executor.setCorePoolSize(10); // 核心线程数(默认线程数)
executor.setMaxPoolSize(100); // 最大线程数
executor.setQueueCapacity(200); // 缓冲队列数
executor.setKeepAliveSeconds(10); // 允许线程空闲时间(单位:秒)
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 线程池对拒绝任务的处理策略
// 初始化
executor.initialize();
return executor;
}
}
总结
以上便是 Spring 的 异步调用 @Async 的用法,使用方式并没有难度,唯一需要注意的是线程池的配置。虽然这样的使用方式确实省了不少事情,但是基于更多的自由因素的考虑,我个人还是建议使用线程池而不是这种注解的方式,而且线程池应该在不同的场景给出不同的配置,而不是使用一个”大而通用“的线程池配置。