一些业务场景我们需要使用多线程异步执行任务,加快任务执行速度。
之前有写过一篇文章叫做: 异步编程利器:CompletableFuture
在实际工作中也更加推荐使用CompletableFuture,因为它实现异步方式更加优雅,而且功能更加强大!
既然SpringBoot能通过 @Async 也实现异步执行任务,那么这篇文章就来总结下如何使用 @Async 实现异步执行任务。
一、SpringBoot使用@Async注解步骤
1、启动类上使用@EnableAsync注解
@SpringBootApplication @EnableAsync public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
2、异步方法所在的类注入容器中
@Componet public class Test{ }
除了@Componet,也可以是@Controller、@RestController、@Service、@Configuration等注解,加入到Ioc容器里。
3、方法上加上@Async注解
@Service public class Test{ @Async public void a() { } }
二、哪些情况会导致@Async异步失效?
如果你明明是按照上面的步骤来的,但是发现@Async注解还是不起作用,这里还有两点注意,因为@Async是基于Aop思想实现的,所有下面两种情况也会失效。
1、异步方法使用static修饰
@Async public static void a() { }
2、调用方法和异步方法在同一个类中
当异步方法和调用方法在同一个类中时,是没办法通过Ioc里的bean来执行异步方法的,从而变成同步方法。
如下:
@Component public class Task { /** * 调异步方法和异步方法在同一个类 @Async执行失败 */ public void dotask() { this.taskOne(); this.taskTwo(); } @Async public void taskOne() { //执行任务1 } @Async public void taskTwo() { //执行任务2 } }
三、SpringBoot结合@Async实现异步示例
首先我们来看同步方法
1、同步调用示例
@Component @Slf4j public class DemoTask { public void taskOne() throws Exception { log.info("===执行任务1==="); long start = System.currentTimeMillis(); Thread.sleep(200); long end = System.currentTimeMillis(); log.info("任务1执行结束,总耗时={} 毫秒", end - start); } public void taskTwo() throws Exception { log.info("===执行任务2==="); long start = System.currentTimeMillis(); Thread.sleep(200); long end = System.currentTimeMillis(); log.info("任务2执行结束,总耗时={} 毫秒", end - start); } public void taskThere() throws Exception { log.info("===执行任务3==="); long start = System.currentTimeMillis(); Thread.sleep(200); long end = System.currentTimeMillis(); log.info("任务3执行结束,总耗时={} 毫秒", end - start); } }
执行方法
@Slf4j @RunWith(SpringRunner.class) @SpringBootTest public class DemoTaskTest { @Autowired private DemoTask demoTask; @Test public void runDemo() throws Exception { long start = System.currentTimeMillis(); demoTask.taskOne(); demoTask.taskTwo(); demoTask.taskThere(); long end = System.currentTimeMillis(); log.info("总任务执行结束,总耗时={} 毫秒", end - start); } }
输出日志
===执行任务1=== 任务1执行结束,总耗时=204 毫秒 ===执行任务2=== 任务2执行结束,总耗时=203 毫秒 ===执行任务3=== 任务3执行结束,总耗时=201 毫秒 总任务执行结束,总耗时=613 毫秒
2、异步调用示例
异步方法
@Component @Slf4j public class AsyncTask { @Async public void taskOne() throws Exception { //执行内容同上,省略 } @Async public void taskTwo() throws Exception { //执行内容同上,省略 } @Async public void taskThere() throws Exception { //执行内容同上,省略 } }
调用方法
@Slf4j @RunWith(SpringJUnit4ClassRunner.class) @EnableAsync @SpringBootTest public class AsyncTest { @Autowired private AsyncTask asyncTask; @Test public void runAsync() throws Exception { long start = System.currentTimeMillis(); asyncTask.taskOne(); asyncTask.taskTwo(); asyncTask.taskThere(); Thread.sleep(200); long end = System.currentTimeMillis(); log.info("总任务执行结束,总耗时={} 毫秒", end - start); } }
查看日志
===执行任务1=== ===执行任务3=== ===执行任务2=== 总任务执行结束,总耗时=206 毫秒 任务1执行结束,总耗时=200 毫秒 任务2执行结束,总耗时=201 毫秒 任务3执行结束,总耗时=201 毫秒
通过日志可以看出已经是已经实现异步处理任务了,而且异步任务哪个先执行是不确定的。
3、Future异步回调
如果我想异步执行,同时想获取所有异步执行的结果,那么这个时候就需要采用Future。
异步方法
@Component @Slf4j public class FutureTask { @Async public Future<String> taskOne() throws Exception { //执行内容同上,省略 return new AsyncResult<>("1完成"); } @Async public Future<String> taskTwo() throws Exception { //执行内容同上,省略 return new AsyncResult<>("2完成"); } @Async public Future<String> taskThere() throws Exception { //执行内容同上,省略 return new AsyncResult<>("执行任务3完成"); } }
调用方法
@Slf4j @RunWith(SpringRunner.class) @SpringBootTest @EnableAsync public class FutureTaskTest { @Autowired private FutureTask futureTask; @Test public void runAsync() throws Exception { long start = System.currentTimeMillis(); Future<String> taskOne = futureTask.taskOne(); Future<String> taskTwo = futureTask.taskTwo(); Future<String> taskThere = futureTask.taskThere(); while (true) { if (taskOne.isDone() && taskTwo.isDone() && taskThere.isDone()) { log.info("任务1返回结果={},任务2返回结果={},任务3返回结果={},", taskOne.get(), taskTwo.get(), taskThere.get()); break; } } long end = System.currentTimeMillis(); log.info("总任务执行结束,总耗时={} 毫秒", end - start); } }
查看日志
===执行任务2=== ===执行任务3=== ===执行任务1=== 任务1执行结束,总耗时=201 毫秒 任务3执行结束,总耗时=201 毫秒 任务2执行结束,总耗时=201 毫秒 任务1返回结果=1完成,任务2返回结果=2完成,任务3返回结果=执行任务3完成, 总任务执行结束,总耗时=223 毫秒
从日志可以看出 异步任务的执行结果都有获取。
四、@Async+自定义线程池实现异步任务
如果不自定义异步方法的线程池默认使用SimpleAsyncTaskExecutor线程池。
SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。并发大的时候会产生严重的性能问题。
Spring也更加推荐我们开发者使用ThreadPoolTaskExecutor类来创建线程池。
自定义线程池
@Configuration public class ExecutorAsyncConfig { @Bean(name = "newAsyncExecutor") public Executor newAsync() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); //设置核心线程数 taskExecutor.setCorePoolSize(10); // 线程池维护线程的最大数量,只有在缓冲队列满了以后才会申请超过核心线程数的线程 taskExecutor.setMaxPoolSize(100); //缓存队列 taskExecutor.setQueueCapacity(50); //允许的空闲时间,当超过了核心线程数之外的线程在空闲时间到达之后会被销毁 taskExecutor.setKeepAliveSeconds(200); //异步方法内部线程名称 taskExecutor.setThreadNamePrefix("my-xiaoxiao-AsyncExecutor-"); //拒绝策略 taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); taskExecutor.initialize(); return taskExecutor; } }
示例代码
任务1和任务2配置走我们自定义的线程池,任务3还是走默认线程池。
@Component @Slf4j public class FutureExecutorTask { @Async("newAsyncExecutor") public Future<String> taskOne() throws Exception { log.info("任务1线程名称 = {}", Thread.currentThread().getName()); return new AsyncResult<>("1完成"); } @Async("newAsyncExecutor") public Future<String> taskTwo() throws Exception { log.info("任务2线程名称 = {}", Thread.currentThread().getName()); return new AsyncResult<>("2完成"); } @Async public Future<String> taskThere() throws Exception { log.info("任务3线程名称 = {}", Thread.currentThread().getName()); return new AsyncResult<>("执行任务3完成"); } }
调研方法和上面一样,我们再来看下日志
任务2线程名称 = my-xiaoxiao-AsyncExecutor-2 任务1线程名称 = my-xiaoxiao-AsyncExecutor-1 任务3线程名称 = SimpleAsyncTaskExecutor-1 总任务执行结束,总耗时=15 毫秒
通过日志我们可以看出 任务1和任务2走的是我们自定义的线程池,任务3还是走默认线程池。
五、CompletableFuture实现异步任务
推荐这种方式来实现异步,它不需要在启动类上加@EnableAsync注解,也不需要在方法上加@Async注解,它实现更加优雅,而且CompletableFuture功能更加强大。
具体可以看下之前写的文章:异步编程利器:CompletableFuture
1、CompletableFuture示例
看如何使用
@RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest public class CompletableTest { @Autowired private DemoTask dmoTask; @Test public void testCompletableThenRunAsync() throws Exception { long startTime = System.currentTimeMillis(); CompletableFuture<Void> cp1 = CompletableFuture.runAsync(() -> { //任务1 dmoTask.taskOne(); }); CompletableFuture<Void> cp2 = CompletableFuture.runAsync(() -> { //任务2 dmoTask.taskTwo(); }); CompletableFuture<Void> cp3 = CompletableFuture.runAsync(() -> { //任务3 dmoTask.taskThere(); }); cp1.get(); cp2.get(); cp3.get(); //模拟主程序耗时时间 System.out.println("总共用时" + (System.currentTimeMillis() - startTime) + "ms"); } }
查看日志
===执行任务1=== ===执行任务2=== ===执行任务3=== 任务3执行结束,总耗时=204 毫秒 任务2执行结束,总耗时=203 毫秒 任务1执行结束,总耗时=204 毫秒 总共用时226ms
从日志可以看出,通过CompletableFuture同样可以实现异步执行任务!
声明: 公众号如需转载该篇文章,发表文章的头部一定要 告知是转至公众号: 后端元宇宙。同时也可以问本人要markdown原稿和原图片。其它情况一律禁止转载!