SpringBoot之@Async异步调用
利用 Spring Initializer 创建一个 gradle 项目 spring-boot-async-task,创建时添加相关依赖。
在 Spring Boot 入口类上配置 @EnableAsync 注解开启异步处理。
创建任务抽象类 AbstractTask,并分别配置三个任务方法 doTaskOne(),doTaskTwo(),doTaskThree()。
下面通过一个简单示例来直观的理解什么是同步调用:
定义 Task 类,继承 AbstractTask,三个处理函数分别模拟三个执行任务的操作,操作消耗时间随机取(10 秒内)。
在 单元测试 用例中,注入 Task 对象,并在测试用例中执行 doTaskOne(),doTaskTwo(),doTaskThree() 三个方法。
执行单元测试,可以看到类似如下输出:
任务一、任务二、任务三顺序的执行完了,换言之 doTaskOne(),doTaskTwo(),doTaskThree() 三个方法顺序的执行完成。
上述的可以看到 执行时间比较长,若这三个任务本身之间 不存在依赖关系,可以 并发执行 的话,同步调用在 执行效率 方面就比较差,可以考虑通过 异步调用 的方式来 并发执行。
创建 AsyncTask类,分别在方法上配置 @Async 注解,将原来的 同步方法 变为 异步方法。
在 单元测试 用例中,注入 AsyncTask 对象,并在测试用例中执行 doTaskOne(),doTaskTwo(),doTaskThree() 三个方法。
执行单元测试,可以看到类似如下输出:
如果反复执行单元测试,可能会遇到各种不同的结果,比如:
原因是目前 doTaskOne(),doTaskTwo(),doTaskThree() 这三个方法已经 异步执行 了。主程序在 异步调用 之后,主程序并不会理会这三个函数是否执行完成了,由于没有其他需要执行的内容,所以程序就 自动结束 了,导致了 不完整 或是 没有输出任务 相关内容的情况。
根据业务需求,可以将暂时不需要立即获得处理的方法设置为 @Async .
比如用户在前端点击完成了登录操作,这时候根据业务要求需要在登录成功之后进行埋点的处理.
其实埋点成功与否都不影响用户操作,这时候就可以将埋点方法设置为@Async.
个人认为此类任务通常有三个特征:
为了让 doTaskOne(),doTaskTwo(),doTaskThree() 能正常结束,假设我们需要统计一下三个任务 并发执行 共耗时多少,这就需要等到上述三个函数都完成动用之后记录时间,并计算结果。
那么我们如何判断上述三个 异步调用 是否已经执行完成呢?我们需要使用 FutureT 来返回 异步调用 的 结果。
在 单元测试 用例中,注入 AsyncCallBackTask 对象,并在测试用例中执行 doTaskOneCallback(),doTaskTwoCallback(),doTaskThreeCallback() 三个方法。循环调用 Future 的 isDone() 方法等待三个 并发任务 执行完成,记录最终执行时间。
在测试用例一开始记录开始时间;在调用三个异步函数的时候,返回Future类型的结果对象;在调用完三个异步函数之后,开启一个循环,根据返回的Future对象来判断三个异步函数是否都结束了。若都结束,就结束循环;若没有都结束,就等1秒后再判断。跳出循环之后,根据结束时间 - 开始时间,计算出三个任务并发执行的总耗时。
执行一下上述的单元测试,可以看到如下结果:
可以看到,通过 异步调用,让任务一、任务二、任务三 并发执行,有效的 减少 了程序的 运行总时间。
在上述操作中,创建一个 线程池配置类 TaskConfiguration ,并配置一个 任务线程池对象 taskExecutor。
上面我们通过使用 ThreadPoolTaskExecutor 创建了一个 线程池,同时设置了以下这些参数:
创建 AsyncExecutorTask类,三个任务的配置和 AsyncTask 一样,不同的是 @Async 注解需要指定前面配置的 线程池的名称 taskExecutor。
在 单元测试 用例中,注入 AsyncExecutorTask 对象,并在测试用例中执行 doTaskOne(),doTaskTwo(),doTaskThree() 三个方法。
执行一下上述的 单元测试,可以看到如下结果:
执行上面的单元测试,观察到 任务线程池 的 线程池名的前缀 被打印,说明 线程池 成功执行 异步任务!
解决方案如下,重新设置线程池配置对象,新增线程池 setWaitForTasksToCompleteOnShutdown() 和 setAwaitTerminationSeconds() 配置:
Spring Boot - 异步任务
有时候,前端可能提交了一个耗时任务,如果后端接收到请求后,直接执行该耗时任务,那么前端需要等待很久一段时间才能接受到响应。如果该耗时任务是通过浏览器直接进行请求,那么浏览器页面会一直处于转圈等待状态。一个简单的例子如下所示:
当我们在浏览器请求 localhost:8080/async/ 页面时,可以看到浏览器一直处于转圈等待状态,这样体验十分不友好。
事实上,当后端要处理一个耗时任务时,通常都会将耗时任务提交到一个异步任务中进行执行,此时前端提交耗时任务后,就可直接返回,进行其他操作。
在 Java 中,开启异步任务最常用的方式就是开辟线程执行异步任务,如下所示:
这时浏览器请求 localhost:8080/async/ ,就可以很快得到响应,并且耗时任务会在后台得到执行。
一般来说,前端不会关注耗时任务结果,因此前端只需负责提交该任务给到后端即可。但是如果前端需要获取耗时任务结果,则可通过 Future 等方式将结果返回,详细内容请参考后文。
事实上,在 Spring Boot 中,我们不需要手动创建线程异步执行耗时任务,因为 Spring 框架已提供了相关异步任务执行解决方案,本文主要介绍下在 Spring Boot 中执行异步任务的相关内容。
Spring 3.0 时提供了一个 @Async 注解,该注解用于标记要进行异步执行的方法,当在其他线程调用被 @Async 注解的方法时,就会开启一个线程执行该方法。
注 : @Async 注解通常用在方法上,但是也可以用作类型上,当类被 @Async 注解时,表示该类中所有的方法都是异步执行的。
在 Spring Boot 中,如果要执行一个异步任务,只需进行如下两步操作:
被 @Async 注解的异步任务方法存在相关限制:
默认情况下,Spring 会自动搜索相关线程池定义:要么是一个唯一 TaskExecutor Bean 实例,要么是一个名称为 taskExecutor 的 Executor Bean 实例。如果这两个 Bean 实例都不存在,就会使用 SimpleAsyncTaskExecutor 来异步执行被 @Async 注解的方法。
综上,可以知道,默认情况下,Spring 使用的 Executor 是 SimpleAsyncTaskExecutor , SimpleAsyncTaskExecutor 每次调用都会创建一个新的线程,不会重用之前的线程。很多时候,这种实现方式不符合我们的业务场景,因此通常我们都会自定义一个 Executor 来替换 SimpleAsyncTaskExecutor 。
对于自定义 Executor(自定义线程池),可以分为如下两个层级:
前文介绍过,对于被 @Async 注解的异步方法,只能返回 void 或者 Future 类型。对于返回 Future 类型数据,如果异步任务方法抛出异常,则很容易进行处理,因为 Future.get() 会重新抛出该异常,我们只需对其进行捕获即可。但是对于返回 void 的异步任务方法,异常不会传播到被调用者线程,因此我们需要自定义一个额外的异步任务异常处理器,捕获异步任务方法抛出的异常。
自定义异步任务异常处理器的步骤如下所示:
Spring Boot 异步任务 -- @EnableAsync 详解
@EnableAsync 注解启用了 Spring 异步方法执行功能,在 Spring Framework API 中有详细介绍。
@EnableAsync 默认启动流程:
1 搜索关联的线程池定义:上下文中唯一的 TaskExecutor 实例,或一个名为 taskExecutor 的 java.util.concurrent.Executor 实例;
2 如果以上都没找到,则会使用 SimpleAsyncTaskExecutor 处理异步方法调用。
注意:具有 void 返回类型的带注释方法不能将任何异常发送回调用者,默认情况下此类未捕获异常只会被记录日志。
定制 @EnableAsync 启动行为:
1 实现 AsyncConfigurer 接口
2 实现 getAsyncExecutor() 方法自定义 java.util.concurrent.Executor
3 实现 getAsyncUncaughtExceptionHandler() 方法自定义 AsyncUncaughtExceptionHandler
示例:修改 AsyncConfig 配置类实现
Spring Boot 微服务异步调用 @EnableAsync @Async
第一步:在Application启动类上面加上@EnableAsync注解
第二步:定义[线程池]
第三步:在异步方法上添加@Async
第四步:测试
输出结果:
时间testA:2
开始testB
开始testA
完成testA
完成testB
任务testA,当前线程:async-thread-pool-1
时间testB:3002
异步方法@Async注解失效情况:
(1)在@SpringBootApplication启动类没有添加注解@EnableAsync
(2)调用方法和异步方法写在同一个类,需要在不同的类才能有效。
(2)调用的是静态(static )方法
(3)调用(private)私有化方法
个别失效报错情况:
报错一:提示需要在@EnableAsync上设置proxyTargetClass=true来强制使用基于cglib的代理。注解上加上即可。
Springboot 使用@Async开启异步调用
大家都知道,java是同步顺序执行。当需要异步执行时,需要新创建一个线程完成。
1. 使用常规的方法显示异步调用
第一步 新建 ThreadTest.java 实现 Runnable 接口
第二步 新建测试执行
当然,除了这种显式 new Thread 对象,我们通过线程池获取线程名称,这里不做演示。我们熟悉的spring 在 spring3中提供了@Async注解,来方便开发者优雅的使用异步调用。
2.使用 springboot @Async注解,优雅的实现异步调用
第一步 开启 异步调用注解。
第二步 定义线程池
第三步 创建service 测试类 TestService.java
第四步 新建 Service 实现类 ,TestServiceImpl.java
第五步 测试执行 ,执行结果
SpringBoot使用@Async优雅的异步调用就暂时记录到这里,欢迎评论区一起讨论学习。
一图看懂Spring Boot 异步框架
在SpringBoot的日常开发中,一般都是同步调用的。但经常有特殊业务需要做异步来处理,例如:注册新用户,送100个积分,或下单成功,发送push消息等等。
就拿注册新用户为什么要异步处理?
在SpringBoot中使用异步调用是很简单的,只需要使用@Async注解即可实现方法的异步调用。
采用@EnableAsync来开启异步任务支持,另外需要加入@Configuration来把当前类加入springIOC容器中。
增加一个service类,用来做积分处理。
@Async添加在方法上,代表该方法为异步处理。
@Async注解,在默认情况下用的是SimpleAsyncTaskExecutor线程池,该线程池不是真正意义上的线程池,因为线程不重用,每次调用都会新建一条线程。
可以通过控制台日志输出查看,每次打印的线程名都是[task-1]、[task-2]、[task-3]、[task-4].....递增的。
@Async注解异步框架提供多种线程
SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。
SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。
ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类。
ThreadPoolTaskScheduler:可以使用cron表达式。
ThreadPoolTaskExecutor :最常使用,推荐。 其实质是对java.util.concurrent.ThreadPoolExecutor的包装。