线程池关闭BUG

遇到一个反直觉BUG

问题的背景是作战地图批量站点查询接口,在某天来到公司的时候BA突然跟我说这个接口异常了

然后我一看,嗷,原来是前台吧约定好的Arr入参私自替换成了数值型的,那这样也好改,我做一下适配就行了,

然后啪的一下很快啊,代码上到测试环境,Postman启动,正常运行,OK没问题,简简单单的问题,接下来去页面测一下吧,504?接口超时?你在逗我?这刚刚测的不还是好好的么,你不信我这就,呃呃呃,怎么Postman也超时了 喂!

好吧,拉前台看一下吧,是不是前台该什么配置了,没有?那还原一下代码吧,回滚成之前String的入参,怎么可能,还是Postman可以页面不行?奇了怪了

好吧,看来问题还没那么好解决,看下日志吧,emmm,这个方法正常进来了,哎不对,为什么调用外部方法的时候一直被阻塞住了?不合理啊,外部服务是正常的。

emmm,首先分析一下,这是个并发批处理接口,用到了线程池,那就日志打详细一点吧,看看具体是哪一步卡住了?这。。。线程池没有正常启动?啊?这。

经过一番仔细的排查,发现是前一天把线程池的创建优化了一下,原先是自己在代码中直接 生成了一个线程池,手动关闭的,但是评审的人说这里可以用配置Spring内置的ThreadPoolTaskExcutor来作为一个Bean来管理。但是问题就 出现在这里 ,Spring内置的线程池是不需要手动去关闭的,Spring容器已经帮我们完成了对线程池的生命周期管理,手动关闭的话就相当于首次使用的时候是正常的,后续使用就无法正常使用了。

总结一下:

一开始排查思路其实是绕弯路的,因为先将方向先定在了前后台入参改变上面,又是排查类型转换又是回滚代码。但最终排查下来并非是前后台入参原因。而是我不经意的一个改动导致的,所以,之后如果遇到了这种先是正常之后被改出问题的BUG场景,需要优先分析从正常到出问题时间间隔之间的这些改动,然后再来来看看是不是外部原因导致的。

DS解释

这个问题的排查过程非常经典,涉及到了线程池生命周期管理和Spring容器管理的误解。我来帮你梳理一下问题的本质和解决方案:

问题根源分析

  1. 线程池管理方式变更

    • 旧方案:手动创建和关闭线程池(ExecutorService
    • 新方案:改用Spring管理的ThreadPoolTaskExecutor
  2. 关键错误

    • 在新方案中仍然保留了手动关闭线程池的代码(可能是从旧代码迁移时遗漏的)
    • Spring管理的Bean默认是单例的,手动关闭后线程池被永久终止
  3. 现象解释

    • 第一次请求成功:因为线程池尚未被关闭
    • 后续请求失败:第一次请求后线程池被手动关闭,无法再处理新任务
    • Postman能通但页面超时:可能是测试时刚好是首次请求

解决方案

  1. 立即修复

    // 错误示例(需要删除的代码)
    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;
    
    public void someMethod() {
        // ...业务逻辑...
        taskExecutor.shutdown();  // ← 这是罪魁祸首
    }
    
  2. 正确做法

    • 完全移除所有手动关闭线程池的代码
    • 依赖Spring容器的生命周期管理
  3. 配置建议

    @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;
        }
    }
    

深入理解

  1. Spring管理的线程池特点

    • 作为单例Bean由Spring容器初始化
    • 随应用启动而创建,随应用关闭而销毁
    • 不需要也不应该手动管理其生命周期
  2. 与手动创建线程池的区别

    特性 手动创建 Spring管理
    生命周期管理 需显式shutdown() 容器自动管理
    异常处理 需自行处理 可通过AOP统一处理
    配置灵活性 硬编码在代码中 可通过properties配置
  3. 最佳实践

    • 对于并发任务,推荐使用@Async注解
    • 监控线程池状态(可通过JMX或自定义端点)
    • 合理设置拒绝策略

后续改进建议

  1. 添加线程池监控:

    @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;
        }
    }
    
  2. 考虑使用更现代的并发处理:

    // 使用CompletableFuture
    public CompletableFuture<Result> processBatch(List<Item> items) {
        return CompletableFuture.supplyAsync(() -> {
            // 处理逻辑
            return result;
        }, taskExecutor);
    }
    

这个案例很好地展示了"看似简单的优化可能引入隐蔽问题"的现象。关键在于理解框架对资源生命周期的管理机制,避免混合使用不同管理模式。

posted @ 2025-04-26 15:17  月朗星希  阅读(11)  评论(0)    收藏  举报