Java线程池第二次提交任务被拒原因及内存泄漏问题咨询
问题根因
异常触发的核心逻辑可以拆解为三点:
- 原始线程池配置为核心线程数=最大线程数=3,工作队列是容量为1的有界
LinkedBlockingQueue,默认采用AbortPolicy拒绝策略,线程池最大可接收的瞬时任务量为「最大线程数+队列容量=4」。 - 带超时参数的
executor.invokeAll()有个容易踩的特性:超时触发后,方法会立刻给所有未完成的Future发送cancel信号然后直接返回,不会等待正在工作线程上执行的任务实际响应中断、退出执行。 - 你的任务
request()内部是长sleep逻辑,收到cancel的中断信号后会抛出InterruptedException退出,但从收到中断到任务实际退出、工作线程回到空闲状态存在时间差。第一批invokeAll返回后你立刻提交第二批3个任务时,3个核心线程还没完全从第一批被取消的任务中释放,线程池判定核心线程全忙,会尝试把任务塞入工作队列:队列容量只有1,塞入1个任务后队列就满了,又因为最大线程数和核心线程数相等,无法创建非核心线程处理剩余任务,直接触发拒绝策略抛出异常。
你换成无界LinkedBlockingQueue后能正常执行的原因很简单:无界队列不会出现队列满的情况,不管核心线程是否忙,所有提交的任务都能塞入队列排队,不会触发拒绝条件,等核心线程处理完第一批任务自然会拉取第二批任务执行。
无界队列是否会导致内存泄漏
超时取消任务本身不会因为用无界队列导致内存泄漏,但无界队列存在固有的容量风险:
- 对于已经被工作线程取走执行的任务:
cancel(true)发送中断信号后,任务执行结束后对应的FutureTask对象会被正常回收,不会残留。 - 对于还在队列中排队没被执行的任务:
FutureTask.cancel()默认会把当前任务从阻塞队列中移除,不会一直留在队列里占用内存。 - 真正的风险是队列容量无上限:如果任务提交速度持续大于线程处理速度,队列里的任务会不断堆积,最终撑满JVM内存抛出OOM,这属于容量失控问题,不是内存泄漏。如果你的任务提交速度可控,每次提交的批量任务数不会远大于线程处理能力,用无界队列不会有泄漏问题。
修复方案
根据业务场景选择任意一种即可:
- 方案1:调整工作队列容量,保证队列容量至少等于单次批量提交的任务数。比如你每次invokeAll提交3个任务,把队列容量从1改成3即可,就算3个核心线程全忙,3个新任务也能全部塞进队列,不会触发拒绝。
// 队列容量设为3,匹配单次批量提交的任务规模 ExecutorService executor = new ThreadPoolExecutor(3, 3, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(3), new NamedThreadFactory("timeOutThread")); - 方案2:第一批invokeAll返回后,不要立刻提交第二批任务,等所有被取消的任务真正执行完成、工作线程回到空闲状态再提交。可以在第一批任务处理完后加短暂等待,或者循环判断线程池活跃线程数降到0再提交下一批。
- 方案3:如果业务能接受任务排队的内存开销,可以保留无界队列配置,但建议加上线程池队列长度、活跃线程数的监控,避免任务异常堆积导致OOM。
- 额外注意:如果你的任务逻辑里有捕获
InterruptedException的代码,不要吞掉中断信号,要么捕获后直接退出任务,要么调用Thread.currentThread().interrupt()恢复中断状态,否则会导致任务无法正常退出、线程长期被占用的问题。
内容的提问来源于stack exchange,提问作者Rebecca
相关产品推荐
相关产品推荐

