java spring线程池详解_Spring Boot利用@Async异步调用:ThreadPoolTaskScheduler线程池的优雅关闭详解...

前言

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

问题现象

在上篇文章的例子Chapter4-1-3中,我们定义了一个线程池,然后利用@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容器中其他资源的顺序是否安全。

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

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() ~[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为我们提供了相关的配置,只需要加入如下设置即可:

@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://xiazai.jb51.net/201805/yuanma/SpringBoot-Learning(jb51.net).rar

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是一个简单的示例: 假设我们有一个 UserService,其中有一个方法 sendEmail,它需要异步地发送电子邮件。现在我们想要将当前用户的信息透传给异步任务中使用的线程。 首先,我们需要在异步方法上添加 @Async 注解,并在配置类中启用异步支持: ```java @Configuration @EnableAsync public class AppConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(100); executor.setQueueCapacity(10); executor.initialize(); return executor; } @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return new CustomAsyncExceptionHandler(); } } ``` 在上面的示例中,我们创建了一个 ThreadPoolTaskExecutor,它将用于执行异步任务。我们还实现了 AsyncConfigurer 接口,并覆盖了 getAsyncExecutor 和 getAsyncUncaughtExceptionHandler 方法,以提供自定义的 Executor 和异常处理程序。 现在我们需要将当前用户信息存储在一个 ThreadLocal 对象中。这可以通过一个拦截器来实现: ```java public class UserContextInterceptor extends HandlerInterceptorAdapter { private final ThreadLocal<String> userThreadLocal = new ThreadLocal<>(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String currentUser = request.getHeader("X-User"); userThreadLocal.set(currentUser); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { userThreadLocal.remove(); } public String getCurrentUser() { return userThreadLocal.get(); } } ``` 在上面的示例中,我们创建了一个 UserContextInterceptor,它将在每个请求的开始和结束时执行。在 preHandle 方法中,我们从请求头中获取当前用户信息,并将其存储在一个 ThreadLocal 对象中。在 afterCompletion 方法中,我们将删除该信息,以避免内存泄漏。 现在,我们可以在 UserService 的 sendEmail 方法中使用 UserContextInterceptor 中存储的当前用户信息: ```java @Service public class UserService { @Autowired private JavaMailSender mailSender; @Autowired private UserContextInterceptor userContextInterceptor; @Async public void sendEmail(String to, String subject, String text) { String currentUser = userContextInterceptor.getCurrentUser(); // 使用当前用户信息发送电子邮件 // ... } } ``` 在上面的示例中,我们使用 @Autowired 注解将 UserContextInterceptor 注入到 UserService 中。在 sendEmail 方法中,我们从 UserContextInterceptor 中获取当前用户信息,并在发送电子邮件时使用它。 通过这种方式,我们可以将当前用户信息透传给异步任务中使用的线程

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值