在Java并发编程中,线程池是处理异步任务的核心组件。当线程池处于饱和状态时,也就是正在运行的线程数已经达到最大线程数,并且工作队列也已经排满,此时如果再提交新的任务,线程池就会触发拒绝策略。JDK中ThreadPoolExecutor提供了几种内置的拒绝策略,它们决定了饱和情况下任务的最终去向。

JDK内置的四种拒绝策略
ThreadPoolExecutor在java.util.concurrent包中定义了RejectedExecutionHandler接口,JDK默认提供了四种实现类,分别位于ThreadPoolExecutor的内部类中。它们都实现了同一个接口方法rejectedExecution(Runnable r, ThreadPoolExecutor e),但内部行为完全不同。
第一种是AbortPolicy,它是线程池的默认策略。该策略会直接抛出RejectedExecutionException异常,将问题抛给调用者处理。这种方式的好处是失败透明,调用方能够感知到任务被拒绝;缺点则是如果调用方没有捕获异常,可能导致业务流程中断。
第二种是CallerRunsPolicy,它的行为是让提交任务的线程自己去执行这个任务。也就是说,当线程池饱和时,调用execute的主线程会转而运行该任务。这种策略能有效减缓任务提交速度,因为提交线程被占用就无法继续提交新任务,起到负反馈作用,但可能造成调用线程阻塞。
第三种是DiscardPolicy,它什么都不做,直接把任务丢弃,也不抛异常。这种策略适用于那些可以接受任务丢失的场景,比如一些非关键的日志统计。但由于完全无感知,排查问题时会比较困难。
第四种是DiscardOldestPolicy,它会尝试丢弃工作队列中最早进入的任务,也就是队首任务,然后重新尝试将当前任务提交到队列中。如果队列还是满的,那这次尝试也会失败。该策略在一定程度上保证了新任务有机会被执行,但可能丢失老任务。
不同策略行为对比
为了直观理解四种策略的差异,我们可以从任务去向、是否抛异常、对调用线程的影响三个维度进行对比。下面的表格列出了核心区别:
| 策略名称 | 任务去向 | 是否抛异常 | 调用线程影响 |
|---|---|---|---|
| AbortPolicy | 不执行,抛出异常 | 是 | 异常向上传播 |
| CallerRunsPolicy | 由提交线程执行 | 否 | 占用提交线程 |
| DiscardPolicy | 直接丢弃 | 否 | 无影响 |
| DiscardOldestPolicy | 丢弃队首后入队 | 否 | 无直接影响 |
从对比可以看出,AbortPolicy强调失败可见,CallerRunsPolicy强调自我调节,而两种Discard系列则更关注吞吐而非任务完整性。在电商秒杀场景中,如果下单任务不允许丢失,通常会选用CallerRunsPolicy或自定义策略;而在指标打点场景中,DiscardPolicy则更为轻量。
除了内置策略,我们还可以通过实现RejectedExecutionHandler接口来编写自定义策略。例如将拒绝的任务写入本地磁盘或转发到另一个队列,以便后续补偿。下面是一段自定义策略的简单示例:
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
// 自定义拒绝策略:打印日志并忽略
public class LogAndDiscardPolicy implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 记录被拒绝的任务信息
System.out.println("任务被拒绝: " + r.toString());
// 选择静默丢弃,也可改为持久化操作
}
}
如何在代码中指定拒绝策略
在创建ThreadPoolExecutor时,可以通过构造函数的最后一个参数传入具体的拒绝策略实例。如果不传,默认就是AbortPolicy。下面演示了如何显式设置一个CallerRunsPolicy:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class PoolDemo {
public static void main(String[] args) {
// 核心线程2,最大线程2,队列容量1
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
2,
0L,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1),
new ThreadPoolExecutor.CallerRunsPolicy()
);
for (int i = 0; i < 5; i++) {
int taskId = i;
executor.execute(() -> {
System.out.println("执行任务 " + taskId + " 线程: " + Thread.currentThread().getName());
});
}
executor.shutdown();
}
}
在上面代码中,由于线程数和队列都很小,当提交到第4个任务时线程池已经饱和,此时第4个任务会由主线程直接执行,从而缓解提交速度。这种方式在突发流量下比直接抛异常更平滑。
需要特别注意的是,如果使用Executors工具类创建的线程池,其拒绝策略也各有不同。例如newFixedThreadPool由于使用无界队列,永远不会触发拒绝策略;而newCachedThreadPool由于最大线程数为Integer.MAX_VALUE,也基本不会拒绝。因此理解拒绝策略的前提是搞清楚线程池的队列类型和容量设置。
选型建议与常见误区
很多人在配置线程池时只关注核心线程数和队列大小,忽略了拒绝策略的适配。实际上,拒绝策略直接决定了系统在极端压力下的表现。如果业务不允许丢任务,就不要使用DiscardPolicy或DiscardOldestPolicy;如果希望快速失败并及时告警,AbortPolicy配合监控系统是更合适的选择。
另一个常见误区是认为CallerRunsPolicy永远安全。当提交任务的线程是Tomcat的业务线程时,如果该线程被长时间占用执行任务,会导致HTTP请求线程池耗尽,进而引发整个接口不可用。因此在使用该策略时,要评估任务执行耗时是否可控。
总结来说,Java线程池的拒绝策略不是孤立存在的,它和线程数配置、队列类型、业务容忍度紧密相关。通过对比不同策略的行为特征,结合实际场景权衡,才能构建出健壮的并发处理层。
Java线程池拒绝策略ThreadPoolExecutor修改时间:2026-08-05 14:39:37