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

前言

SpringBoot 中的方法调用,默认是单线程顺序执行的。但是在开发中我们可能会存在这样一些场景,例如发送邮件或者记录日志等,这些操作往往比较耗时,但是又不是主业务中跟业务相关的内容。这种场景我们就可以选择使用 @Async 异步方法执行,即用其它线程来异步执行某些耗时操作,从而节省主线程的运行等待时间。

使用 @Async 异步执行方法

想要使方法异步执行非常简单,简单来说,只需要在需要异步执行的方法上添加 @Async 注解即可。

编写一个 @Service 服务类,模拟耗时操作。在方法的前后,我们打上开始和结束日志,并输出线程名。

@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 拆开,放在两个类中。

编写一个定时任务,定时执行该方法。

@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 注解,用于开始异步执行功能。

@SpringBootApplication
@EnableScheduling
@EnableAsync
public class AsyncExecutionApplication {
    public static void main(String[] args) {
        SpringApplication.run(AsyncExecutionApplication.class, args);
    }
}

启动程序,我们可以看到如下日志:

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 配置类。

@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 来管理。

给异步方法指定线程池

我们再创建一个异步方法,这次将该方法指定给我们刚刚配置好的线程池来处理。

@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 线程池来处理。

启动程序,运行结果如下:

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


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

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

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

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

你可能感兴趣的:(SpringBoot,开发实践,SpringBoot)