上周發(fā)了一篇關(guān)于Spring Boot中使用
@Async
來(lái)實(shí)現(xiàn)異步任務(wù)和線程池控制的文章:《Spring Boot使用@Async實(shí)現(xiàn)異步調(diào)用:自定義線程池》枷莉。由于最近身邊也發(fā)現(xiàn)了不少異步任務(wù)沒有正確處理而導(dǎo)致的不少問題婆排,所以在本文就接前面內(nèi)容廊镜,繼續(xù)說說線程池的優(yōu)雅關(guān)閉固阁,主要針對(duì)ThreadPoolTaskScheduler
線程池迂卢。
問題現(xiàn)象
在上篇文章的例子Chapter4-1-3中美莫,我們定義了一個(gè)線程池菜拓,然后利用@Async
注解寫了3個(gè)任務(wù)倍踪,并指定了這些任務(wù)執(zhí)行使用的線程池系宫。在上文的單元測(cè)試中,我們沒有具體說說shutdown相關(guān)的問題建车,下面我們就來(lái)模擬一個(gè)問題現(xiàn)場(chǎng)出來(lái)扩借。
第一步:如前文一樣,我們定義一個(gè)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;
}
}
}
第二步:改造之前的異步任務(wù)缤至,讓它依賴一個(gè)外部資源潮罪,比如:Redis
@Slf4j
@Component
public class Task {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Async("taskExecutor")
public void doTaskOne() throws Exception {
log.info("開始做任務(wù)一");
long start = System.currentTimeMillis();
log.info(stringRedisTemplate.randomKey());
long end = System.currentTimeMillis();
log.info("完成任務(wù)一,耗時(shí):" + (end - start) + "毫秒");
}
@Async("taskExecutor")
public void doTaskTwo() throws Exception {
log.info("開始做任務(wù)二");
long start = System.currentTimeMillis();
log.info(stringRedisTemplate.randomKey());
long end = System.currentTimeMillis();
log.info("完成任務(wù)二领斥,耗時(shí):" + (end - start) + "毫秒");
}
@Async("taskExecutor")
public void doTaskThree() throws Exception {
log.info("開始做任務(wù)三");
long start = System.currentTimeMillis();
log.info(stringRedisTemplate.randomKey());
long end = System.currentTimeMillis();
log.info("完成任務(wù)三嫉到,耗時(shí):" + (end - start) + "毫秒");
}
}
注意:這里省略了pom.xml中引入依賴和配置redis的步驟
第三步:修改單元測(cè)試,模擬高并發(fā)情況下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循環(huán)往上面定義的線程池中提交任務(wù)戒突,由于是異步執(zhí)行屯碴,在執(zhí)行過程中,利用System.exit(0)
來(lái)關(guān)閉程序膊存,此時(shí)由于有任務(wù)在執(zhí)行导而,就可以觀察這些異步任務(wù)的銷毀與Spring容器中其他資源的順序是否安全忱叭。
第四步:運(yùn)行上面的單元測(cè)試,我們將碰到下面的異常內(nèi)容今艺。
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
來(lái)看韵丑,我們很容易的可以想到,在應(yīng)用關(guān)閉的時(shí)候異步任務(wù)還在執(zhí)行虚缎,由于Redis連接池先銷毀了撵彻,導(dǎo)致異步任務(wù)中要訪問Redis的操作就報(bào)了上面的錯(cuò)。所以实牡,我們得出結(jié)論陌僵,上面的實(shí)現(xiàn)方式在應(yīng)用關(guān)閉的時(shí)候是不優(yōu)雅的,那么我們要怎么做呢创坞?
解決方法
要解決上面的問題很簡(jiǎn)單碗短,Spring的ThreadPoolTaskScheduler
為我們提供了相關(guān)的配置,只需要加入如下設(shè)置即可:
@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)
該方法就是這里的關(guān)鍵题涨,用來(lái)設(shè)置線程池關(guān)閉的時(shí)候等待所有任務(wù)都完成再繼續(xù)銷毀其他的Bean偎谁,這樣這些異步任務(wù)的銷毀就會(huì)先于Redis線程池的銷毀。同時(shí)纲堵,這里還設(shè)置了setAwaitTerminationSeconds(60)
巡雨,該方法用來(lái)設(shè)置線程池中任務(wù)的等待時(shí)間,如果超過這個(gè)時(shí)候還沒有銷毀就強(qiáng)制銷毀席函,以確保應(yīng)用最后能夠被關(guān)閉铐望,而不是阻塞住。
完整示例:
讀者可以根據(jù)喜好選擇下面的兩個(gè)倉(cāng)庫(kù)中查看Chapter4-1-4
項(xiàng)目:
- Github:https://github.com/dyc87112/SpringBoot-Learning/
- Gitee:https://gitee.com/didispace/SpringBoot-Learning/
如果您對(duì)這些感興趣向挖,歡迎star蝌以、follow、收藏何之、轉(zhuǎn)發(fā)給予支持跟畅!