0%

SpringBoot 开发实践(6):@Async 异步执行方法及配置自定义线程池

前言

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-");

// rejection-policy:当pool已经达到max size的时候,如何处理新任务
// CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
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


我是因特马,一个爱分享的斜杠程序员~

欢迎关注我的公众号:一只因特马

  • 本文作者: 因特马
  • 本文链接: https://www.interhorse.cn/a/3350135757/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!