线程池关闭BUG
遇到一个反直觉BUG
问题的背景是作战地图批量站点查询接口,在某天来到公司的时候BA突然跟我说这个接口异常了
然后我一看,嗷,原来是前台吧约定好的Arr入参私自替换成了数值型的,那这样也好改,我做一下适配就行了,
然后啪的一下很快啊,代码上到测试环境,Postman启动,正常运行,OK没问题,简简单单的问题,接下来去页面测一下吧,504?接口超时?你在逗我?这刚刚测的不还是好好的么,你不信我这就,呃呃呃,怎么Postman也超时了 喂!
好吧,拉前台看一下吧,是不是前台该什么配置了,没有?那还原一下代码吧,回滚成之前String的入参,怎么可能,还是Postman可以页面不行?奇了怪了
好吧,看来问题还没那么好解决,看下日志吧,emmm,这个方法正常进来了,哎不对,为什么调用外部方法的时候一直被阻塞住了?不合理啊,外部服务是正常的。
emmm,首先分析一下,这是个并发批处理接口,用到了线程池,那就日志打详细一点吧,看看具体是哪一步卡住了?这。。。线程池没有正常启动?啊?这。
经过一番仔细的排查,发现是前一天把线程池的创建优化了一下,原先是自己在代码中直接 生成了一个线程池,手动关闭的,但是评审的人说这里可以用配置Spring内置的ThreadPoolTaskExcutor来作为一个Bean来管理。但是问题就 出现在这里 ,Spring内置的线程池是不需要手动去关闭的,Spring容器已经帮我们完成了对线程池的生命周期管理,手动关闭的话就相当于首次使用的时候是正常的,后续使用就无法正常使用了。
总结一下:
一开始排查思路其实是绕弯路的,因为先将方向先定在了前后台入参改变上面,又是排查类型转换又是回滚代码。但最终排查下来并非是前后台入参原因。而是我不经意的一个改动导致的,所以,之后如果遇到了这种先是正常之后被改出问题的BUG场景,需要优先分析从正常到出问题时间间隔之间的这些改动,然后再来来看看是不是外部原因导致的。
DS解释
这个问题的排查过程非常经典,涉及到了线程池生命周期管理和Spring容器管理的误解。我来帮你梳理一下问题的本质和解决方案:
问题根源分析
线程池管理方式变更:
- 旧方案:手动创建和关闭线程池(
ExecutorService)- 新方案:改用Spring管理的
ThreadPoolTaskExecutor关键错误:
- 在新方案中仍然保留了手动关闭线程池的代码(可能是从旧代码迁移时遗漏的)
- Spring管理的Bean默认是单例的,手动关闭后线程池被永久终止
现象解释:
- 第一次请求成功:因为线程池尚未被关闭
- 后续请求失败:第一次请求后线程池被手动关闭,无法再处理新任务
- Postman能通但页面超时:可能是测试时刚好是首次请求
解决方案
立即修复:
// 错误示例(需要删除的代码) @Autowired private ThreadPoolTaskExecutor taskExecutor; public void someMethod() { // ...业务逻辑... taskExecutor.shutdown(); // ← 这是罪魁祸首 }正确做法:
- 完全移除所有手动关闭线程池的代码
- 依赖Spring容器的生命周期管理
配置建议:
@Configuration public class ThreadPoolConfig { @Bean public ThreadPoolTaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(100); executor.setThreadNamePrefix("Async-"); executor.initialize(); return executor; } }深入理解
Spring管理的线程池特点:
- 作为单例Bean由Spring容器初始化
- 随应用启动而创建,随应用关闭而销毁
- 不需要也不应该手动管理其生命周期
与手动创建线程池的区别:
特性 手动创建 Spring管理 生命周期管理 需显式shutdown() 容器自动管理 异常处理 需自行处理 可通过AOP统一处理 配置灵活性 硬编码在代码中 可通过properties配置 最佳实践:
- 对于并发任务,推荐使用
@Async注解- 监控线程池状态(可通过JMX或自定义端点)
- 合理设置拒绝策略
后续改进建议
添加线程池监控:
@Endpoint(id = "thread-pool") @Component public class ThreadPoolEndpoint { @Autowired private ThreadPoolTaskExecutor executor; @ReadOperation public Map<String, Object> threadPoolMetrics() { Map<String, Object> metrics = new HashMap<>(); metrics.put("activeCount", executor.getActiveCount()); metrics.put("poolSize", executor.getPoolSize()); metrics.put("queueSize", executor.getThreadPoolExecutor().getQueue().size()); return metrics; } }考虑使用更现代的并发处理:
// 使用CompletableFuture public CompletableFuture<Result> processBatch(List<Item> items) { return CompletableFuture.supplyAsync(() -> { // 处理逻辑 return result; }, taskExecutor); }这个案例很好地展示了"看似简单的优化可能引入隐蔽问题"的现象。关键在于理解框架对资源生命周期的管理机制,避免混合使用不同管理模式。

浙公网安备 33010602011771号