前言 SpringBoot 中的方法调用,默认是单线程顺序执行的。但是在开发中我们可能会存在这样一些场景,例如发送邮件或者记录日志等,这些操作往往比较耗时,但是又不是主业务中跟业务相关的内容。这种场景我们就可以选择使用 @Async
异步方法执行,即用其它线程来异步执行某些耗时操作,从而节省主线程的运行等待时间。
使用 @Async 异步执行方法 想要使方法异步执行非常简单,简单来说,只需要在需要异步执行的方法上添加 @Async
注解即可。
编写一个 @Service
服务类,模拟耗时操作。在方法的前后,我们打上开始和结束日志,并输出线程名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Service public class AsyncServiceImpl implements AsyncService { private static final Logger LOG = LoggerFactory.getLogger(AsyncServiceImpl.class); @Async @Override public void printLog1 () { LOG.info("printLog1 开始执行 -> Thread name is: {}" , Thread.currentThread().getName()); try { Thread.sleep(3000 ); } catch (InterruptedException e) { e.printStackTrace(); } LOG.info("printLog1 执行完毕 -> Thread name is: {}" , Thread.currentThread().getName()); } }
@Async: 表明该方法异步执行。
注意: 异步方法和调用该异步方法的方法不能放在同一个类中,否则 @Async
注解将失效。例如,方法 A 和异步方法 B 都在同一个类中,那么 A 中调用 B 时,B 还是会按照单线程来运行。解决方法就是,将 A、B 拆开,放在两个类中。
编写一个定时任务,定时执行该方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Component public class SchedulerTask { private static final Logger LOG = LoggerFactory.getLogger(SchedulerTask.class); @Autowired private AsyncService asyncService; @Scheduled(cron = "*/5 * * * * ?") public void scheduler1 () { LOG.info("scheduler1 开始执行" ); asyncService.printLog1(); LOG.info("scheduler1 执行完毕" ); } }
最后,别忘了在 Application 入口类上打上 @EnableAsync
注解,用于开始异步执行功能。
1 2 3 4 5 6 7 8 @SpringBootApplication @EnableScheduling @EnableAsync public class AsyncExecutionApplication { public static void main (String[] args) { SpringApplication.run(AsyncExecutionApplication.class, args); } }
启动程序,我们可以看到如下日志:
1 2 3 4 5 6 7 8 9 10 11 12 2020-06-22 21:57:30.001 INFO 57067 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler1 开始执行 2020-06-22 21:57:30.018 INFO 57067 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler1 执行完毕 2020-06-22 21:57:30.019 INFO 57067 --- [TaskExecutor-1] c.i.s.a.service.impl.AsyncServiceImpl : printLog1 开始执行 -> Thread name is: SimpleAsyncTaskExecutor-1 2020-06-22 21:57:33.020 INFO 57067 --- [TaskExecutor-1] c.i.s.a.service.impl.AsyncServiceImpl : printLog1 执行完毕 -> Thread name is: SimpleAsyncTaskExecutor-1 2020-06-22 21:57:35.003 INFO 57067 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler1 开始执行 2020-06-22 21:57:35.004 INFO 57067 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler1 执行完毕 2020-06-22 21:57:35.004 INFO 57067 --- [TaskExecutor-2] c.i.s.a.service.impl.AsyncServiceImpl : printLog1 开始执行 -> Thread name is: SimpleAsyncTaskExecutor-2 2020-06-22 21:57:38.008 INFO 57067 --- [TaskExecutor-2] c.i.s.a.service.impl.AsyncServiceImpl : printLog1 执行完毕 -> Thread name is: SimpleAsyncTaskExecutor-2 2020-06-22 21:57:40.001 INFO 57067 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler1 开始执行 2020-06-22 21:57:40.001 INFO 57067 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler1 执行完毕 2020-06-22 21:57:40.002 INFO 57067 --- [TaskExecutor-3] c.i.s.a.service.impl.AsyncServiceImpl : printLog1 开始执行 -> Thread name is: SimpleAsyncTaskExecutor-3 2020-06-22 21:57:43.003 INFO 57067 --- [TaskExecutor-3] c.i.s.a.service.impl.AsyncServiceImpl : printLog1 执行完毕 -> Thread name is: SimpleAsyncTaskExecutor-3
我们可以看到,定时任务与 printLog1()
方法并发执行,说明 printLog1()
方法成功地异步执行了。
自定义线程池 从上面的日志我们可以看到,使用默认 @Async
异步执行的方法,用的是 SimpleAsyncTaskExecutor
不重用线程,每次调用都创建了一个新的线程。
默认的 @Async
虽然可以应付一般的场景,但是如果是并发量比较高的情况下,就存在一定风险了。例如开销过大、内存溢出等。为使服务运行稳定,我们可以自定义配置线程池,然后让给需要异步执行的方法指定用该线程池运行。
配置自定义线程池 创建一个 ExecutorConfig.java
配置类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Configuration public class ExecutorConfig { @Bean(name = "myExecutor") public Executor executor () { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor (); executor.setCorePoolSize(3 ); executor.setMaxPoolSize(10 ); executor.setQueueCapacity(100 ); executor.setThreadNamePrefix("MyExecutor-" ); executor.setRejectedExecutionHandler(new ThreadPoolExecutor .CallerRunsPolicy()); executor.initialize(); return executor; } }
@Configuration: 表名这是一个配置类。该类中打上 @Bean
注解的方法都会在 Spring 启动时被扫描运行,然后将返回的 bean 注入到 Spring 容器中。 @Bean: 该方法返回的对象将被注入到 Spring 容器中。在上面的方法中,我们将自定义配置的线程池命名为 myExecutor
交给 Spring 来管理。
给异步方法指定线程池 我们再创建一个异步方法,这次将该方法指定给我们刚刚配置好的线程池来处理。
1 2 3 4 5 6 7 8 9 10 11 @Async("myExecutor") @Override public void printLog2 () { LOG.info("printLog2 开始执行 -> Thread name is: {}" , Thread.currentThread().getName()); try { Thread.sleep(3000 ); } catch (InterruptedException e) { e.printStackTrace(); } LOG.info("printLog2 执行完毕 -> Thread name is: {}" , Thread.currentThread().getName()); }
@Async: 将自定义线程池的名字赋值给 @Async
,那么就表明该方法需要用 myExecutor
线程池来处理。
启动程序,运行结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 2020-06-23 01:19:40.001 INFO 57595 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler2 开始执行 2020-06-23 01:19:40.018 INFO 57595 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler2 执行完毕 2020-06-23 01:19:40.018 INFO 57595 --- [ MyExecutor-1] c.i.s.a.service.impl.AsyncServiceImpl : printLog2 开始执行 -> Thread name is: MyExecutor-1 2020-06-23 01:19:43.022 INFO 57595 --- [ MyExecutor-1] c.i.s.a.service.impl.AsyncServiceImpl : printLog2 执行完毕 -> Thread name is: MyExecutor-1 2020-06-23 01:19:45.000 INFO 57595 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler2 开始执行 2020-06-23 01:19:45.001 INFO 57595 --- [ MyExecutor-2] c.i.s.a.service.impl.AsyncServiceImpl : printLog2 开始执行 -> Thread name is: MyExecutor-2 2020-06-23 01:19:45.001 INFO 57595 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler2 执行完毕 2020-06-23 01:19:48.002 INFO 57595 --- [ MyExecutor-2] c.i.s.a.service.impl.AsyncServiceImpl : printLog2 执行完毕 -> Thread name is: MyExecutor-2 2020-06-23 01:19:50.004 INFO 57595 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler2 开始执行 2020-06-23 01:19:50.005 INFO 57595 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler2 执行完毕 2020-06-23 01:19:50.005 INFO 57595 --- [ MyExecutor-3] c.i.s.a.service.impl.AsyncServiceImpl : printLog2 开始执行 -> Thread name is: MyExecutor-3 2020-06-23 01:19:53.006 INFO 57595 --- [ MyExecutor-3] c.i.s.a.service.impl.AsyncServiceImpl : printLog2 执行完毕 -> Thread name is: MyExecutor-3 2020-06-23 01:19:55.001 INFO 57595 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler2 开始执行 2020-06-23 01:19:55.002 INFO 57595 --- [ MyExecutor-1] c.i.s.a.service.impl.AsyncServiceImpl : printLog2 开始执行 -> Thread name is: MyExecutor-1 2020-06-23 01:19:55.002 INFO 57595 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler2 执行完毕 2020-06-23 01:19:58.003 INFO 57595 --- [ MyExecutor-1] c.i.s.a.service.impl.AsyncServiceImpl : printLog2 执行完毕 -> Thread name is: MyExecutor-1
可以看到,日志中输出的线程前缀名称,即为我们自定义线程池前缀的名称。
以上就是使用 @Async 异步执行及配置自定义线程池的方法。
本章代码地址:GitHub
我是因特马,一个爱分享的斜杠程序员~
欢迎关注我的公众号:因特马