第14章 线程池 RejectedExecutionException 与拒绝策略源码

第14章 线程池 RejectedExecutionException 与拒绝策略源码

14.1 触发条件的精确边界:实测验证

很多人对"线程池什么时候会拒绝任务"只有模糊印象,用最小化参数实测精确边界:

java 复制代码
ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
        new ArrayBlockingQueue<>(1)); // 核心线程1,最大线程1,队列容量1
for (int i = 0; i < 5; i++) {
    pool.execute(task_i);
}

实测结果(JDK 21):

ini 复制代码
submitted 0    ← 核心线程立即执行(active threads = 1)
submitted 1    ← 进入队列(queued tasks = 1)
task 2 rejected: ... pool size = 1, active threads = 1, queued tasks = 1 ...
task 3 rejected: ...
task 4 rejected: ...
task 0 done
task 1 done

精确验证了 ThreadPoolExecutor 的任务分发逻辑(源码 execute() 方法的三段式判断):

  1. 当前运行线程数 < corePoolSize → 创建新线程直接执行。
  2. 否则尝试放入队列 → 队列未满则入队等待。
  3. 队列也满了 → 尝试创建新线程(前提是当前线程数 < maximumPoolSize)。
  4. 以上都不满足(本例中 maximumPoolSize 也等于 1,无法再创建新线程)→ 触发拒绝策略。

本例中 core=max=1,第 3 步永远不可能成立,所以核心线程执行 1 个 + 队列容纳 1 个之后,第 3、4、5 个任务必然全部被拒绝,异常信息里明确带出了拒绝时刻线程池的完整状态快照(pool size / active threads / queued tasks / completed tasks),这是排查线程池打满问题时最直接的诊断依据。

14.2 四种内置拒绝策略源码级对比

java 复制代码
// 1. AbortPolicy(默认):直接抛异常,最"暴力"但也最能第一时间暴露问题
public static class AbortPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString());
    }
}

// 2. CallerRunsPolicy:把任务丢回给"提交任务的线程"自己同步执行
public static class CallerRunsPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            r.run(); // 注意:是直接调用 run(),不是提交到线程池,因此这里会阻塞调用线程
        }
    }
}

// 3. DiscardPolicy:静默丢弃,不抛异常也不记录任何日志------生产环境慎用,容易丢任务却毫无感知
public static class DiscardPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { }
}

// 4. DiscardOldestPolicy:丢弃队列中最老(排队最久)的任务,把位置让给新任务
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            e.getQueue().poll(); // 从队列头部丢弃一个
            e.execute(r);         // 重新提交当前任务
        }
    }
}

CallerRunsPolicy 的"降速"效果原理 :当线程池打满,提交任务的线程(通常是接收请求的主线程,比如 Web 容器的请求处理线程)会被迫自己同步执行这个任务,这段时间内该线程无法再去接收/提交新的请求,客观上起到了给上游限速的作用------这是一种朴素但有效的背压(backpressure)机制,代价是可能拖慢请求处理线程的响应速度。

生产环境选型建议 :核心业务链路通常不建议用 DiscardPolicy (静默丢任务,出问题很难被发现,往往是通过"业务数据对不上"这种滞后信号才暴露),更推荐 AbortPolicy 配合主动 catch 后走降级逻辑(比如写入延迟队列/落库后续补偿),或者 CallerRunsPolicy 配合合理的超时控制。


相关推荐
金銀銅鐵12 小时前
[Java] 展示增删元素时 LinkedList 的内部变化
java·后端·ai编程
武子康13 小时前
Vercel Cache Reason:相同 MISS,怎样决定下一步查什么
人工智能·后端·agent
杨文说AI与数字化13 小时前
把模型依赖做成可替换的组件:三层适配 + 一套回归评估
后端
IT_陈寒14 小时前
Vue的这个响应式陷阱我居然现在才发现
前端·人工智能·后端
WL学习笔记14 小时前
使用string类
c++·后端·string类
飞鸟真人14 小时前
IM网关系列(九)单账号流量统计:滑动窗口桶方案设计与Go实现
后端·golang
武子康15 小时前
Embedding 替换怎么评测:把索引成本算进成功任务
人工智能·后端·agent
大哥430917 小时前
on-failure 不扛重启:一次 Docker restart 策略的反直觉排查
后端
房西杭17 小时前
GM指令:仓库扩容、清空仓库(行会仓库 + 个人仓库,Lua + proto + GMMgr注册)
后端
大哥430917 小时前
不报错 ≠ 生效:三类"静默失效"缺陷与它们的共同指纹
后端