线程池用得好是性能利器,用不好就是事故源头。很多开发者对核心参数了如指掌,却对拒绝策略一知半解,直接使用默认的AbortPolicy,结果线上流量一冲,满屏都是RejectedExecutionException,业务直接雪崩。拒绝策略没有绝对的好坏,关键在于匹配业务场景。下面四种典型场景,帮你一次选对。
场景一:允许失败,但要留痕——AbortPolicy
AbortPolicy是默认策略,直接抛出RejectedExecutionException,让调用方感知到任务被拒绝。很多人觉得它粗暴,但在一些场景下,它恰恰是最诚实的选择。
比如日志收集、埋点上报这类非核心链路。任务丢了就丢了,业务不能因此受影响,但开发需要知道丢了。用AbortPolicy配合上层捕获异常并记录监控指标,既能保护主流程,又能暴露容量问题。如果换成静默丢弃,日志丢了都不知道,排查时只能抓瞎。
适用原则:任务可丢,但丢要丢得明白。
场景二:不能丢,但可以慢——CallerRunsPolicy
CallerRunsPolicy的行为很特别:当线程池满了,谁提交任务,谁就自己执行。这意味着提交任务的线程会被阻塞,直到任务完成。它天然具备反压能力——上游提交越快,被阻塞得越狠,从而自动降低提交速率。
典型的场景是订单创建、支付回调这类绝对不能丢的任务。线程池满了说明处理能力到顶了,此时让调用者线程亲自执行,虽然会拖慢上游,但保证了任务最终被处理,不会凭空消失。比如 Tomcat 的请求线程提交订单任务,线程池满后由 Tomcat 线程执行,请求响应变慢,但订单不会丢。这比直接拒绝或丢弃要安全得多。
适用原则:任务不能丢,允许上游降速,用阻塞换可靠性。
场景三:要最新的,弃最老的——DiscardOldestPolicy
DiscardOldestPolicy会丢弃队列中最老的任务,然后尝试提交新任务。它适合那种“旧数据无意义,最新数据才有价值”的场景。
比如实时监控数据采集、股票行情推送。如果系统繁忙,积压的旧监控点已经失去时效性,处理它们只会浪费资源,不如直接丢掉,腾出位置给最新数据。再比如心跳检测,过期的检测结果没有意义,最新一次才能反映当前状态。
但要注意,这个策略会静默丢弃任务,不会通知任何人。使用前必须确认业务能接受这种丢失,并且最好配合监控统计丢弃数量,否则出了问题都不知道丢了多少。
适用原则:时效性极强,旧任务价值归零,新任务优先。
场景四:不能丢,也不能慢,那就自定义——DiscardPolicy 的替代方案
DiscardPolicy是最危险的策略,它静默丢弃任务,既不抛异常也不执行,等于什么都没发生。绝大多数场景都不应该直接使用它。但有一种情况例外:你明确知道任务可以丢,且不想被任何异常或阻塞干扰。即便如此,也建议用自定义策略替换,在丢弃时记录日志或打点。
更常见的做法是自定义拒绝策略。比如将任务写入数据库或消息队列,等系统空闲时补偿处理;或者返回一个 Future 让调用方决定后续动作。线程池提供了RejectedExecutionHandler接口,实现它只需要几行代码,却能换来更可控的行为。
适用原则:默认丢弃不可取,要么留痕,要么补偿。
怎么选?一张表说清楚
| 业务特征 | 推荐策略 | 核心逻辑 |
|---|---|---|
| 可丢,需感知 | AbortPolicy | 抛异常,快速失败 |
| 不可丢,可降速 | CallerRunsPolicy | 调用者执行,反压上游 |
| 旧任务无价值 | DiscardOldestPolicy | 弃老留新,保时效 |
| 不可丢,不可慢 | 自定义策略 | 落库/队列/补偿 |
最后提醒三点:第一,拒绝策略必须配合有界队列,无界队列永远不会触发拒绝,只会把内存撑爆;第二,任何拒绝都要有监控,统计拒绝次数和原因,否则等于埋雷;第三,线上环境慎用DiscardPolicy,静默失败比报错更可怕。
线程池拒绝策略的选择,本质是对业务容忍度的判断。想清楚任务能不能丢、能不能等、谁该负责,答案自然就出来了。