Spring Boot使用@Async实现异步调用:线程池的优雅关闭

架构师的修炼之路 2019-05-10 19:55:43

上周发了一篇关于Spring Boot中使用 @Async来实现异步任务和线程池控制的文章:《Spring Boot使用@Async实现异步调用:自定义线程池》。由于最近身边也发现了不少异步任务没有正确处理而导致的问题,所以本文就接前面的内容,继续说说线程池的优雅关闭,主要针对 ThreadPoolTaskScheduler线程池。

问题现象

在上篇文章的例子中,我们定义了一个线程池,然后利用 @Async注解写了3个任务,并指定了这些任务执行使用的线程池。在上文的单元测试中,我们没有具体说说shutdown相关的问题,下面我们就来模拟一个问题现场出来。

第一步:如前文一样,我们定义一个 ThreadPoolTaskScheduler线程池:

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

 @EnableAsync
 @Configuration
 class TaskPoolConfig {

 @Bean("taskExecutor")
 public Executor taskExecutor() {
 ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
 executor.setPoolSize(20);
 executor.setThreadNamePrefix("taskExecutor-");
 return executor;
 }
 }
 }

第二步:改造之前的异步任务,让它依赖一个外部资源,比如:Redis

 @Slf4j
 @Component
 public class Task {

 @Autowired
 private StringRedisTemplate stringRedisTemplate;

 @Async("taskExecutor")
 public void doTaskOne() throws Exception {
 log.info("开始做任务一");
 long start = System.currentTimeMillis();
 log.info(stringRedisTemplate.randomKey());
 long end = System.currentTimeMillis();
 log.info("完成任务一,耗时:" + (end - start) + "毫秒");
 }

 @Async("taskExecutor")
 public void doTaskTwo() throws Exception {
 log.info("开始做任务二");
 long start = System.currentTimeMillis();
 log.info(stringRedisTemplate.randomKey());
 long end = System.currentTimeMillis();
 log.info("完成任务二,耗时:" + (end - start) + "毫秒");
 }

 @Async("taskExecutor")
 public void doTaskThree() throws Exception {
 log.info("开始做任务三");
 long start = System.currentTimeMillis();
 log.info(stringRedisTemplate.randomKey());
 long end = System.currentTimeMillis();
 log.info("完成任务三,耗时:" + (end - start) + "毫秒");
 }
 }

注意:这里省略了pom.xml中引入依赖和配置redis的步骤

第三步:修改单元测试,模拟高并发情况下ShutDown的情况:

 @RunWith(SpringJUnit4ClassRunner.class)
 @SpringBootTest
 public class ApplicationTests {

 @Autowired
 private Task task;

 @Test
 @SneakyThrows
 public void test() {
 for (int i = 0; i < 10000; i++) {
 task.doTaskOne();
 task.doTaskTwo();
 task.doTaskThree();
 if (i == 9999) {
 System.exit(0);
 }
 }
 }
 }

说明:通过for循环往上面定义的线程池中提交任务,由于是异步执行,在执行过程中,利用 System.exit(0)来关闭程序,此时由于有任务在执行,就可以观察这些异步任务的销毁与Spring容器中其他资源的顺序是否安全。

第四步:运行上面的单元测试,我们将碰到下面的异常内容。

如何解决

原因分析

从异常信息 JedisConnectionException:Couldnotgeta resourcefromthe pool来看,我们很容易的可以想到,在应用关闭的时候异步任务还在执行,由于Redis连接池先销毁了,导致异步任务中要访问Redis的操作就报了上面的错。所以,我们得出结论,上面的实现方式在应用关闭的时候是不优雅的,那么我们要怎么做呢?

解决方法

要解决上面的问题很简单,Spring的 ThreadPoolTaskScheduler为我们提供了相关的配置,只需要加入如下设置即可:

 @Bean("taskExecutor")
 public Executor taskExecutor() {
 ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
 executor.setPoolSize(20);
 executor.setThreadNamePrefix("taskExecutor-");
 executor.setWaitForTasksToCompleteOnShutdown(true);
 executor.setAwaitTerminationSeconds(60);
 return executor;
 }

说明:

  setWaitForTasksToCompleteOnShutdown(true)该方法就是这里的关键,用来设置线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean,这样这些异步任务的销毁就会先于Redis线程池的销毁。同时,这里还设置了             setAwaitTerminationSeconds(60),该方法用来设置线程池中任务的等待时间,如果超过这个时候还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是阻塞住。

完整示例:

读者可以根据喜好选择下面的两个仓库中查看 Chapter4-1-4项目:

  • Github:https://github.com/dyc87112/SpringBoot-Learning/
  • Gitee:https://gitee.com/didispace/SpringBoot-Learning/

猜你喜欢

转载自blog.csdn.net/u011277123/article/details/90199356