前言
之前分享了一篇關于Spring Boot中使用@Async來實現異步任務和線程池控制的文章:《Spring Boot使用@Async實現異步調用:自定義線程池》。由于最近身邊也發現了不少異步任務沒有正確處理而導致的不少問題,所以在本文就接前面內容,繼續說說線程池的優雅關閉,主要針對ThreadPoolTaskScheduler線程池。
問題現象
在上篇文章的例子Chapter4-1-3中,我們定義了一個線程池,然后利用@Async注解寫了3個任務,并指定了這些任務執行使用的線程池。在上文的單元測試中,我們沒有具體說說shutdown相關的問題,下面我們就來模擬一個問題現場出來。
第一步:如前文一樣,我們定義一個ThreadPoolTaskScheduler線程池:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
@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的情況:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@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容器中其他資源的順序是否安全。
第四步:運行上面的單元測試,我們將碰到下面的異常內容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
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的操作就報了上面的錯。所以,我們得出結論,上面的實現方式在應用關閉的時候是不優雅的,那么我們要怎么做呢?
解決方法
要解決上面的問題很簡單,Spring的ThreadPoolTaskScheduler為我們提供了相關的配置,只需要加入如下設置即可:
1
2
3
4
5
6
7
8
9
|
@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/
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對服務器之家的支持。
原文鏈接:http://blog.didispace.com/springbootasync-3/