【JUC】线程池有哪些拒绝策略?该如何选择使用?
线程池的拒绝策略有哪些?
线程池的拒绝策略(Rejection Policy)是指当线程池无法接受新提交的任务时采取的应对措施。这通常发生在以下情况:
- 线程池已关闭(
shutdown
)。 - 线程池的工作线程已满(达到
maximumPoolSize
)且等待队列已满(达到workQueue
的容量)。
Java 的ThreadPoolExecutor
类提供了四种内置的拒绝策略,它们都实现了RejectedExecutionHandler
接口。
(1) AbortPolicy
(中止策略 - 默认策略)
- 行为:直接抛出
RejectedExecutionException
异常。 - 优点:策略明确,抛出异常便于开发者感知问题,及时做后续处理(如回滚、重试等)。
- 缺点:会中断当前提交任务的流程。
- 使用场景:关键业务,需要确保提交的每一个任务都被执行,并且能够及时感知到系统过载的情况。例如,交易、订单等系统。
(2) CallerRunsPolicy
(调用者运行策略)
- 行为:不抛弃任务,也不抛出异常。而是将某些被拒绝的任务回退给调用者线程(即提交该任务的线程)来执行。
- 优点:
- 不会丢弃任务,保证了所有任务都能得到执行。
- 提交任务的线程被占用来执行任务,在此期间它无法继续提交新任务,这相当于给了线程池一个“减速”的机会,有助于缓和提交任务的速度。
- 缺点:可能会影响调用者线程(如 Tomcat 的 HTTP 处理线程)的响应速度。
- 使用场景:需要保证所有任务都必须被执行,且可以接受短期同步执行的场景。常用于不允许任务丢失,但对实时性要求不非常极端的应用。
(3) DiscardPolicy
(丢弃策略)
- 行为:静默地直接丢弃无法处理的新任务,不抛出任何异常,就像这个任务从来没被提交过一样。
- 优点:对调用者无感知,不会影响当前业务流程。
- 缺点:任务被无声无息地丢弃,可能导致数据不一致或业务逻辑缺失,问题难以排查。
- 使用场景:非关键业务,允许丢失一些任务。例如,日志收集、无关紧要的统计信息上报等。
(4) DiscardOldestPolicy
(丢弃最老策略)
- 行为:丢弃等待队列中最老(即下一个即将被线程执行)的任务,然后尝试重新提交当前这个被拒绝的新任务。
- 优点:给了新任务一次机会,避免了最新的任务被丢弃。
- 缺点:
- 会丢弃一个正在排队的老任务。
- 任务丢弃是静默的,同样存在数据不一致的风险。
- 如果任务之间有依赖顺序,可能会导致问题。
- 使用场景:新任务比老任务优先级更高的场景。例如,消息队列中,新的消息可能比积压的旧消息更有价值。
(5) 自定义拒绝策略
如果以上四种内置策略都不满足需求,你可以通过实现 RejectedExecutionHandler
接口来自定义拒绝策略。
public class MyRejectionPolicy implements RejectedExecutionHandler {@Overridepublic void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {// 自定义处理逻辑,例如:// 1. 将任务持久化到磁盘,待系统恢复后重新加载// 2. 记录日志并发送告警// 3. 尝试等待一段时间后重新放入队列System.out.println("Task " + r.toString() + " was rejected. Doing something special...");// 尝试重新提交一次(注意:这可能不是个好主意,容易造成循环)// if (!executor.isShutdown()) {// executor.execute(r);// }}
}// 使用自定义策略ThreadPoolExecutor customExecutor = new ThreadPoolExecutor(..., new MyRejectionPolicy());
几种拒绝策略该如何选择?
拒绝策略的总结:
策略名称 | 行为 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
AbortPolicy | 抛出异常 | 明确感知问题 | 中断流程 | 关键业务(默认) |
CallerRunsPolicy | 回调给调用者 | 不丢弃任务,减缓提交速度 | 可能阻塞调用者 | 不允许丢失,可同步执行 |
DiscardPolicy | 静默丢弃新任务 | 不影响调用流程 | 任务丢失无感知 | 非关键业务(如日志) |
DiscardOldestPolicy | 丢弃队列头任务 | 给新任务一次机会 | 丢弃老任务,有顺序风险 | 新任务优先级更高 |
- 如果不确定,使用默认的 AbortPolicy 是最安全的选择,因为它能让你第一时间发现问题。
- 如果任务绝对不能丢,但又不想抛异常中断流程,可以考虑 CallerRunsPolicy。
- 如果任务可以丢弃,根据重要性选择是丢弃新的(DiscardPolicy)还是丢弃老的(DiscardOldestPolicy)。
- 通常,DiscardPolicy 和 DiscardOldestPolicy 用于那些即使丢失部分任务也完全不影响主流程的场景。
最好的策略是合理配置线程池参数(如核心线程数、最大线程数、队列类型和容量)来尽量避免拒绝的发生,拒绝策略只是一种最后的保护手段。