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

问题现象

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

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

@SpringBootApplicationpublic 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@Componentpublic 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)@SpringBootTestpublic 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容器中其他资源的顺序是否安全。

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

org.springframework.data.redis.RedisConnectionFailureException: Cannot get Jedis connection; nested exception is redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool   at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:204) ~[spring-data-redis-1.8.10.RELEASE.jar:na] at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java:348) ~[spring-data-redis-1.8.10.RELEASE.jar:na]   at org.springframework.data.redis.core.RedisConnectionUtils.doGetConnection(RedisConnectionUtils.java:129) ~[spring-data-redis-1.8.10.RELEASE.jar:na] at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:92) ~[spring-data-redis-1.8.10.RELEASE.jar:na]    at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:79) ~[spring-data-redis-1.8.10.RELEASE.jar:na]    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:194) ~[spring-data-redis-1.8.10.RELEASE.jar:na]   at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:169) ~[spring-data-redis-1.8.10.RELEASE.jar:na]   at org.springframework.data.redis.core.RedisTemplate.randomKey(RedisTemplate.java:781) ~[spring-data-redis-1.8.10.RELEASE.jar:na] at com.didispace.async.Task.doTaskOne(Task.java:26) ~[classes/:na]    at com.didispace.async.Task$$FastClassBySpringCGLIB$$ca3ff9d6.invoke(<generated>) ~[classes/:na]    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.14.RELEASE.jar:4.3.14.RELEASE]  at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:738) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]  at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]  at org.springframework.aop.interceptor.AsyncExecutionInterceptor$1.call(AsyncExecutionInterceptor.java:115) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]   at java.util.concurrent.FutureTask.run(FutureTask.java:266) [na:1.8.0_151]    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_151]   at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) [na:1.8.0_151]  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_151] at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_151] at java.lang.Thread.run(Thread.java:748) [na:1.8.0_151]Caused by: redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool   at redis.clients.util.Pool.getResource(Pool.java:53) ~[jedis-2.9.0.jar:na]    at redis.clients.jedis.JedisPool.getResource(JedisPool.java:226) ~[jedis-2.9.0.jar:na]    at redis.clients.jedis.JedisPool.getResource(JedisPool.java:16) ~[jedis-2.9.0.jar:na] at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:194) ~[spring-data-redis-1.8.10.RELEASE.jar:na] ... 19 common frames omittedCaused by: java.lang.InterruptedException: null at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.reportInterruptAfterWait(AbstractQueuedSynchronizer.java:2014) ~[na:1.8.0_151]   at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2088) ~[na:1.8.0_151] at org.apache.commons.pool2.impl.LinkedBlockingDeque.pollFirst(LinkedBlockingDeque.java:635) ~[commons-pool2-2.4.3.jar:2.4.3] at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:442) ~[commons-pool2-2.4.3.jar:2.4.3]  at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:361) ~[commons-pool2-2.4.3.jar:2.4.3]  at redis.clients.util.Pool.getResource(Pool.java:49) ~[jedis-2.9.0.jar:na]    ... 22 common frames omitted

如何解决

原因分析

从异常信息JedisConnectionException: Could not get a resource from the 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

如果您对这些感兴趣,欢迎star、follow、收藏、转发给予支持!


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

  1. Spring Boot使用@Async实现异步调用:自定义线程池

    在之前的Spring Boot基础教程系列中,已经通过<Spring Boot中使用@Async实现异步调用>一文介绍过如何使用 @Async注解来实现异步调用了.但是,对于这些异步执行的 ...

  2. springboot 异步mysql_spring boot 使用@Async实现异步调用方法

    使用@Async实现异步调用 什么是"异步调用"与"同步调用" "同步调用"就是程序按照一定的顺序依次执行,,每一行程序代码必须等上一行代码 ...

  3. Spring Boot 2.0选择HikariCP作为默认数据库连接池的五大理由

    转载自公众号:工匠小猪猪的技术世界 摘要: 本文非原创,是「工匠小猪猪的技术世界」搜集了一些HikariCP相关的资料整理给大家的介绍,主要讲解了为什么sb2选择了HikariCP以及HikariCP ...

  4. @Async 异步任务自定义线程池的配置方法和 @Scheduled 定时任务自定义线程池的配置方式

    文章目录 一.定时和异步业务场景描述 二.定时调度任务的实现方式 三.定时调度任务的问题描述 四.定时调度多线程解决方案(方案一) 五.异步多线程程序实现方式 六.定时调度多线程解决方案(方案二) 一 ...

  5. spring boot 学习(十一)使用@Async实现异步调用

    使用@Async实现异步调用 什么是"异步调用"与"同步调用" "同步调用"就是程序按照一定的顺序依次执行,,每一行程序代码必须等上一行代码 ...

  6. java 异步调用 事务_深入理解Spring注解@Async解决异步调用问题

    序言:Spring中@Async 根据Spring的文档说明,默认采用的是单线程的模式的.所以在Java应用中,绝大多数情况下都是通过同步的方式来实现交互处理的. 那么当多个任务的执行势必会相互影响. ...

  7. 使用@Async进行异步调用详解

    本文说下使用@Async进行异步调用详解 文章目录 概述 概述

  8. Spring Boot项目中使用RestTemplate调用https接口出现 unable to find valid certification path to requested target

    问题描述:Spring Boot项目中使用RestTemplate调用https接口出现以下错误: PKIX path building failed: sun.security.provider.c ...

  9. 注解@Async解决异步调用问题

    注解@Async解决异步调用问题 参考文章: (1)注解@Async解决异步调用问题 (2)https://www.cnblogs.com/mmzs/p/11557583.html 备忘一下.

最新文章

  1. ACMNO.18 C语言-选择排序 用选择法对10个整数从小到大排序。 输入 10个整数 输出 排序好的10个整数
  2. 效果提升7%、速度增加220%,OCR开源神器PaddleOCR再迎升级
  3. python散点图拟合曲线-【python常用图件绘制#01】线性拟合结果图
  4. Objective C 中的nil,Nil,NULL和NSNull理解
  5. 选择排序——一般选择排序,堆排序
  6. 【漫画】程序猿 996 会猝死,而企业家 007 却不会?
  7. 介绍一下画图小能手matplotlib。
  8. qchart折现图_Qt开发技术:QCharts(二)QCharts折线图介绍、Demo以及代码详解
  9. Java 中子类是否只继承父类的非私有变量和方法?
  10. 告别写笔记,AI 实时转写黑科技来了!
  11. flock用法详解 linux_netstat命令详解
  12. jquery一些 事件的用法
  13. Webpack学习大纲
  14. 代码行数统计工具 cloc
  15. CCS软件报错“unresolved symbol remain”
  16. 【IDEA 教程系列第 14 篇】idea 快速跳转到错误位置
  17. java自动红包_Java一个简单的红包生成算法
  18. unity做一个小游戏(适合零基础或者巩固加深unity中的工具类的用法)
  19. 精心分享7个办公软件,每一款都好用到爆
  20. win10默认浏览器里多余了一个无效选项

热门文章

  1. DJANGO,获取当前用户名,用户组名,用户组权限
  2. 一个使用指针的简单程序
  3. JAVA NIO 简介(转)
  4. 【SSH进阶之路】一步步重构MVC实现Struts框架——封装业务逻辑和跳转路径(四)...
  5. tomcat内存溢出问题解决思路
  6. 有用的Copy-On-write,写时复制
  7. Android ADB 源码分析(三)
  8. SecureCRT远程登录ubuntu
  9. 回溯 皇后 算法笔记_算法笔记-回溯法
  10. kafka 启动_Kafka管理工具Kafka Manager