线程池拒绝策略CallerRunsPolicy反而卡死了主线程

发布时间:2026/7/24 19:54:50
线程池拒绝策略CallerRunsPolicy反而卡死了主线程 线程池的拒绝策略选了CallerRunsPolicy结果主线程被阻塞服务无响应——这个 bug 在生产环境里出现过多次根因很清晰但不理解线程池工作原理的人很难想到。线程池的四种拒绝策略线程池有一个核心参数设置核心线程数corePoolSize、最大线程数maximumPoolSize、等待队列workQueue。当队列满了且线程数达到最大值时新提交的任务会触发拒绝策略AbortPolicy默认抛出RejectedExecutionException任务被丢弃CallerRunsPolicy由提交任务的线程来执行这个任务DiscardPolicy静默丢弃不抛异常DiscardOldestPolicy丢弃队列里最老的任务然后重新提交当前任务CallerRunsPolicy看起来是最”好”的策略任务没有被丢弃由调用方自己执行保证了任务不丢失。但这个”好”在某些场景下会变成系统崩溃的根因。CallerRunsPolicy 的问题// 典型的配置用线程池处理异步任务 ThreadPoolExecutor executor new ThreadPoolExecutor( 10, 20, 60, TimeUnit.SECONDS, new ArrayBlockingQueue(100), new ThreadPoolExecutor.CallerRunsPolicy() ); // HTTP 请求处理线程里提交任务 PostMapping(/process) public ResponseEntity? process(Request req) { executor.submit(() - heavyTask(req)); // 提交异步任务 return ResponseEntity.ok(submitted); }正常情况HTTP 请求线程提交任务到线程池立刻返回”submitted”很快。线程池满载情况队列满 线程数达到最大值CallerRunsPolicy让 HTTP 请求线程直接执行heavyTask(req)。heavyTask可能要执行几秒这段时间内 HTTP 请求线程被占住——Tomcat 的请求处理线程全被heavyTask占了新的 HTTP 请求无法被处理请求积压服务开始超时。这是一个正反馈的恶性循环线程池满了 → 请求线程执行重任务 → 请求线程占满 → 新请求无法处理 → 更多任务积压 → 线程池更满。什么时候 CallerRunsPolicy 是合适的CallerRunsPolicy在特定场景下是正确的选择——调用方线程被阻塞是可以接受的甚至是期望的起到自然的背压作用。典型场景批处理任务提交。一个批处理程序的主线程读文件把每行数据提交到线程池处理ExecutorService pool new ThreadPoolExecutor( 8, 8, 0, TimeUnit.SECONDS, new ArrayBlockingQueue(1000), new ThreadPoolExecutor.CallerRunsPolicy() ); Files.lines(path).forEach(line - { pool.submit(() - processLine(line)); // 队列满时主线程自己处理 });这里主线程被阻塞是正确的——它的工作就是生产任务被阻塞意味着”等消费跟上”不会有其他副作用。这是CallerRunsPolicy作为背压机制的正确用法。正确的防护如果调用方是 Web 请求处理线程拒绝策略应该快速失败不能阻塞// 方案一AbortPolicy 业务层捕获异常 ThreadPoolExecutor executor new ThreadPoolExecutor( 10, 20, 60, TimeUnit.SECONDS, new ArrayBlockingQueue(100), new ThreadPoolExecutor.AbortPolicy() ); PostMapping(/process) public ResponseEntity? process(Request req) { try { executor.submit(() - heavyTask(req)); return ResponseEntity.ok(submitted); } catch (RejectedExecutionException e) { // 线程池满快速返回错误不阻塞 return ResponseEntity.status(429).body(服务繁忙请稍后重试); } } // 方案二自定义拒绝策略记录日志 返回错误 executor new ThreadPoolExecutor( 10, 20, 60, TimeUnit.SECONDS, new ArrayBlockingQueue(100), (task, pool) - { log.warn(线程池已满任务被拒绝队列大小: {}, pool.getQueue().size()); throw new RejectedExecutionException(线程池已满); } ); // 方案三tryOffer 代替 submit不阻塞地检查是否能提交 if (!executor.getQueue().offer(task)) { // 队列满了做降级处理 handleOverload(task); }监控线程池状态线程池被打满通常不是突发的是逐渐接近边界的。提前监控可以预防// 暴露线程池指标给 MicrometerSpring Boot Actuator Metrics.gauge(executor.queue.size, executor, e - e.getQueue().size()); Metrics.gauge(executor.active.threads, executor, ThreadPoolExecutor::getActiveCount); Metrics.gauge(executor.pool.size, executor, ThreadPoolExecutor::getPoolSize);或者直接用 Spring 的ThreadPoolTaskExecutor配合 Actuator 的/actuator/metrics端点自动暴露线程池指标。队列使用率超过 80% 就应该告警而不是等到 100% 了才发现问题。