rejectedexecutionexception本质是线程池execute()时判定无法接纳任务,原因有三:已关闭、队列满且线程达上限、零缓冲配置(如synchronousqueue+小maximumpoolsize)。

RejectedExecutionException 并不只是“队列满了才抛”,它本质是线程池在 execute() 调用瞬间判定“此刻无法接纳新任务”的信号。这个“无法接纳”可能源于三种独立或叠加的情况:线程池已关闭、任务队列已满且线程数已达上限、或配置组合本身导致零缓冲(如 SynchronousQueue + 小 maximumPoolSize)。真正关键的不是堆参数,而是先厘清当前拒绝发生的真实上下文。
先确认线程池状态,再谈饱和
很多线上问题一上来就调大 corePoolSize 或扩容队列,结果发现日志里刚启动就报错,而监控显示队列使用率不到 5%。这往往是因为线程池被意外 shutdown() 或 shutdownNow() —— 此时哪怕队列空着、线程一个没跑,任何 execute() 都会立即抛出 RejectedExecutionException。
- 上线后第一件事:检查 isShutdown() 和 isTerminated() 返回值,结合日志时间戳定位关闭动作来源
- 排查是否在 Spring 容器销毁、配置热更新、或异常兜底逻辑中误调了 shutdown()
- 若确为正常关闭流程,应改用 submit() + Future.get(timeout) 主动等待,而非无保护地 execute()
看队列类型,判断“满”的真实含义
“队列已满”不等于“用了 100% 容量”。不同队列行为差异极大:
- LinkedBlockingQueue(无界):默认容量 Integer.MAX_VALUE,实际不会因“满”触发拒绝——除非线程池已 shutdown
- ArrayBlockingQueue(有界):容量固定,一旦达到设定值(如 100),且 maximumPoolSize 已到顶,新任务立刻走拒绝策略
- SynchronousQueue(0 容量):不缓存任务,全靠线程“即时接单”。只要没有空闲线程,新任务直接拒绝——对突发流量极敏感
所以看到“queued tasks = 10000”报错,先查 workQueue 类型;如果是 LinkedBlockingQueue 却报满,基本可断定线程池早已 shutdown。
拒绝策略不是补丁,是流量控制开关
选错策略会让小问题演变成雪崩:
- AbortPolicy(默认):适合强一致性场景(如支付扣款),但要求上游必须有重试+幂等;否则就是静默失败
- CallerRunsPolicy:让 HTTP 请求线程自己执行任务,能自然限流,但若任务耗时长,会拖慢整个请求响应,引发超时级联
- DiscardPolicy / DiscardOldestPolicy:适用于埋点、监控、缓存刷新等可丢数据场景;DiscardOldest 适合行情、推送类“新比旧重要”的业务
- 自定义策略:建议只做轻量动作——打告警日志、上报 Prometheus 拒绝计数;避免在 rejectedExecution() 里重试或异步落盘,否则可能二次触发拒绝
参数协同才是根本解法
单独调大某一个参数常无效,必须看三者联动:
- corePoolSize 和 maximumPoolSize 决定了线程扩容边界;keepAliveTime 控制空闲线程存活时间,影响突发流量下能否快速扩缩容
- workQueue 类型决定缓冲能力:SynchronousQueue 配大 maximumPoolSize 可扛瞬时峰值,但内存压力大;LinkedBlockingQueue 配小 maximumPoolSize 更稳,但延迟高
- 典型陷阱:ArrayBlockingQueue(100) + maximumPoolSize=5 → 队列满后线程池不再扩容,任务直接被拒;SynchronousQueue + maximumPoolSize=2 → 两个线程忙时,第三个任务必拒










