上週發了一篇關於Spring Boot中使用@Async
來實現異步任務和線程池控制的文章: 《Spring Boot使用@Async實現異步調用:自定義線程池》。因爲最近身邊也發現了很多異步任務沒有正確處理而致使的很多問題,因此在本文就接前面內容,繼續說說線程池的優雅關閉,主要針對ThreadPoolTaskScheduler
線程池。
在上篇文章的例子Chapter4-1-3中,咱們定義了一個線程池,而後利用@Async
註解寫了3個任務,並指定了這些任務執行使用的線程池。在上文的單元測試中,咱們沒有具體說說shutdown相關的問題,下面咱們就來模擬一個問題現場出來。java
第一步:如前文同樣,咱們定義一個ThreadPoolTaskScheduler
線程池:git
@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; } } }
第二步:改造以前的異步任務,讓它依賴一個外部資源,好比:Redisgithub
@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的步驟redis
第三步:修改單元測試,模擬高併發狀況下ShutDown的狀況:spring
@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容器中其餘資源的順序是否安全。apache
第四步:運行上面的單元測試,咱們將碰到下面的異常內容。安全
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 omitted Caused 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的操做就報了上面的錯。因此,咱們得出結論,上面的實現方式在應用關閉的時候是不優雅的,那麼咱們要怎麼作呢?springboot
要解決上面的問題很簡單,Spring的ThreadPoolTaskScheduler
爲咱們提供了相關的配置,只須要加入以下設置便可:bash
@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
項目:
若是您對這些感興趣,歡迎star、follow、收藏、轉發給予支持!