线程池到底怎么调?从源码看懂 ThreadPoolExecutor 的 7 个参数

线程池到底怎么调?从源码看懂 ThreadPoolExecutor 的 7 个参数

面试问线程池,大多数人能背出 7 个参数,但一问你"核心线程 10、最大线程 20、队列 100,来 130 个任务会怎样?"就卡壳了。

更扎心的是生产事故:某系统高峰期线程数飙到 2000,内存被打爆;另一个系统线程池全部排队,接口超时一片。本质都是没搞懂线程池的调度模型。

这篇从源码出发,把 ThreadPoolExecutor 的 7 个参数和任务调度链路彻底讲清楚。

一、线程池状态机:两个核心字段

先看 ThreadPoolExecutor 最核心的 ctl 字段,一个 int 存了两样东西:

java 复制代码
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

// 高 3 位:线程池状态(runState)
// 低 29 位:线程数量(workerCount)
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;

private static int runStateOf(int c) { return c & ~COUNT_MASK; }  // 取状态
private static int workerCountOf(int c) { return c & COUNT_MASK; } // 取线程数

状态枚举:

状态 含义 能否接收新任务 能否处理队列任务
RUNNING 运行中
SHUTDOWN 关闭(不再接新)
STOP 停止 ❌(中断)
TIDYING 收尾
TERMINATED 终止

生命周期:RUNNING → SHUTDOWN/STOP → TIDYING → TERMINATED

二、7 个参数,一次讲透

java 复制代码
new ThreadPoolExecutor(
    corePoolSize,      // 核心线程数
    maximumPoolSize,   // 最大线程数
    keepAliveTime,     // 非核心线程空闲存活时间
    unit,              // 存活时间单位
    workQueue,         // 任务队列
    threadFactory,     // 线程工厂
    handler            // 拒绝策略
);
参数 作用 易错点
corePoolSize 常驻线程数(默认不回收) 核心线程也可能被回收(allowCoreThreadTimeOut)
maximumPoolSize 最多能开的线程数 必须 ≥ corePoolSize
keepAliveTime 非核心线程空闲多久回收 回收的是超出 core 的线程
workQueue 排队队列 选错队列直接影响行为
threadFactory 线程命名/守护 不设置全叫 pool-N-thread-M,排查时懵
handler 拒绝策略 默认 AbortPolicy 直接抛异常
allowCoreThreadTimeOut (可选)核心线程也超时回收 少见,但能降空闲成本

三、任务来了怎么走?核心调度流程

用一句话概括:核心不满加核心,队列不满进队列,满了开新线程到最大,再满就拒。

arduino 复制代码
            提交任务
               │
   ┌───────────▼────────────┐
   │ workerCount < coreSize │ ──是──→ 新建核心线程执行
   └───────────┬────────────┘
               │ 否
   ┌───────────▼────────────┐
   │ 尝试入队 workQueue      │ ──成功→ 排队等待(后面讲)
   └───────────┬────────────┘
               │ 队列满
   ┌───────────▼────────────┐
   │ workerCount < max?     │ ──是──→ 新建非核心线程执行
   └───────────┬────────────┘
               │ 否
   ┌───────────▼────────────┐
   │ 执行拒绝策略 handler    │
   └────────────────────────┘

关键源码:execute 方法

java 复制代码
public void execute(Runnable command) {
    int c = ctl.get();
    // 第一步:线程数 < 核心线程数 → 直接新建
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true)) return;
        c = ctl.get();
    }
    // 第二步:RUNNING 状态下入队
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 双重检查:线程池若被关闭,回滚并拒绝
        if (!isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 第三步:队列满了 → 尝试开新线程到 maximum
    else if (!addWorker(command, false))
        // 第四步:加不进去(超过 maximum)→ 拒绝
        reject(command);
}

这个顺序非常重要 :核心→队列→最大线程→拒绝。很多人以为"线程不够了就直接开新线程",实际上队列优先于新线程

四、队列的 4 种选择(直接决定线程池行为)

队列 特点 典型效果
ArrayBlockingQueue(有界) 容量固定 满才开新线程,可控
LinkedBlockingQueue(无界/有界) 默认无界 开不到非核心线程,任务全排队
SynchronousQueue 容量 0,直接交接 无队可言,来一个开一个
PriorityBlockingQueue 带优先级 优先级高的先执行

经典坑Executors.newFixedThreadPool 用的是无界队列 LinkedBlockingQueue。队列无限大 → maximumPoolSize 永远用不上 → 任务无限堆积 → 内存 OOM。这就是生产上禁用 Executors 默认工厂的根源。

五、线程池吃饱了:拒绝策略怎么选

策略 行为 适用
AbortPolicy(默认) 抛 RejectedExecutionException 有兜底且想快速失败
CallerRunsPolicy 提交任务线程自己跑 降速+不丢任务,适合能接受慢
DiscardPolicy 静默丢弃 可丢的任务(日志类)
DiscardOldestPolicy 丢弃队头最旧任务 追求最新数据的场景

生产建议CallerRunsPolicyAbortPolicy + 降级,别裸 Discard(丢了没感知)。

六、真实调优:一个订单系统的压测过程

假设接口:单次查询约 80ms,QPS 目标 300,高峰期约 1200,需要能扛 3 倍峰值的短暂积压。

方案推导:

ini 复制代码
目标 QPS = 300,单任务 80ms
单线程吞吐 = 1000 / 80 ≈ 12.5 req/s
所需线程 ≈ 300 / 12.5 = 24 个(理论)

core = 24          // 日常够用
maximum = 60       // 峰值可以扩张到 60
queue = 1000       // 允许短暂积压 1000 个任务
keepAlive = 60s    // 高峰过后 60 秒回收多余线程

注意:这只是"计算起点",必须用压测实测,观察 TP99、队列积压、线程数曲线再迭代。

七、线程池监控:4 个必须看的指标

java 复制代码
ThreadPoolExecutor pool = ...;
int active     = pool.getActiveCount();          // 正在跑
int queueSize  = pool.getQueue().size();          // 排队
long taskCount = pool.getTaskCount();             // 累计任务
long completed = pool.getCompletedTaskCount();    // 已完成

监控重点

  • queue.size() 长期 > 0 → 线程不够 or 任务过多;
  • active 长期接近 maximum → 考虑扩容;
  • 拒绝计数持续上升 → 马上告警。

建议配 ThreadFactory 命名:order-pool-%d,方便线程 dump 时定位。

八、总结

  1. 调度顺序:核心线程 → 队列 → 最大线程 → 拒绝。先队列后扩线程。
  2. 7 个参数:核心/最大/存活/队列/工厂/拒绝/时间单位,个个有坑。
  3. 队列决定行为:无界队列 = 线程永远不扩 + OOM 风险。
  4. 拒绝策略:默认 Abort 抛异常,生产用 CallerRuns 或自定义降级。
  5. 调优靠压测:公式只是起点,监控才是终点。

下一篇:ReentrantLock 与 AQS 源码拆解,看看"锁"到底是怎么排队的。

相关推荐
SQL-First布道者1 小时前
持久层框架的评价标准:只有一个
java·spring boot·spring·tomcat·mybatis·spring jdbc
孔明click331 小时前
Sa-Token v1.46.0 发布 🚀,来看看有没有令你心动的功能!
java·sa-token·开源·springboot·权限认证
m0_587383002 小时前
智慧场馆解决方案实战指南:从系统架构到落地部署全解析
java·spring boot·架构·系统架构
ZC跨境爬虫2 小时前
LeetCode 13. 罗马数字转整数(多解法详解 + Java Python 实现)
java·python·leetcode
省长2 小时前
Sa-Token v1.46.0 发布 🚀,来看看有没有令你心动的功能!
java·后端·开源
LayZhangStrive2 小时前
融360 一面
java·面试·后端开发
城管不管3 小时前
重生——第十次面试之开源中国一面挂
java·linux·开发语言·算法·面试·职场和发展·开源
Java小白笔记3 小时前
Windows系统免软件命令激活
java·网络·人工智能·windows·ai·ai编程
爱敲键盘的猴子3 小时前
深入理解 Java HashMap
java·hashmap