Skip to content

线程池参数估算与监控

线程池参数没有万能公式。真正的参数设计要结合任务类型、机器资源、下游容量、峰值流量、接口超时和业务是否允许排队。

参数估算思路

mermaid
flowchart TD
    A["分析任务类型"] --> B{"CPU 密集还是 IO 密集"}
    B -- "CPU 密集" --> C["线程数接近 CPU 核心数"]
    B -- "IO 密集" --> D["线程数可大于 CPU 核心数"]
    D --> E["检查数据库连接池和下游限流"]
    C --> F["压测验证"]
    E --> F
    F --> G["配置监控和告警"]
    G --> H["根据线上数据调整"]

不要一上来问“线程池该配多大”。应该先问:

  1. 任务主要消耗 CPU,还是主要等待 IO。
  2. 下游数据库、Redis、HTTP 接口能承受多少并发。
  3. 任务平均耗时和 P95/P99 耗时是多少。
  4. 高峰期每秒提交多少任务。
  5. 任务能不能排队,最多能排多久。
  6. 满载时应该失败、降级、反压还是丢弃。

CPU 密集型任务

CPU 密集型任务主要消耗计算资源,例如加密、压缩、图片处理、大量规则计算。

经验:

text
线程数 ≈ CPU 核心数 或 CPU 核心数 + 1

线程太多不会让 CPU 变快,只会增加上下文切换。

Demo:

java
int cpu = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor pool = new ThreadPoolExecutor(
        cpu,
        cpu + 1,
        30,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<Runnable>(100)
);

IO 密集型任务

IO 密集型任务大量时间在等待数据库、Redis、HTTP、文件、MQ,例如接口聚合、批量采集、异步入库。

经验公式可以作为起点:

text
线程数 ≈ CPU 核心数 * (1 + 等待时间 / 计算时间)

但这个公式只能帮助理解,不能直接照抄。因为真实系统还有数据库连接池、HTTP 连接池、下游限流、接口超时和机器内存限制。

下游容量约束

线程池不能只看自己,还要看下游。

mermaid
flowchart TD
    A["业务线程池 50 线程"] --> B["数据库连接池 20 连接"]
    A --> C["HTTP 连接池 30 连接"]
    A --> D["Redis 客户端连接"]
    B --> E["超过容量只能等待"]
    C --> E

如果线程池最大线程数是 100,但数据库连接池只有 20,那么多出来的线程大概率都在等连接。吞吐不一定提高,延迟反而变差。

队列长度怎么估

队列不是越大越好。队列越大,系统越不容易拒绝,但用户等待越久,内存占用越高。

可以从可接受等待时间反推:

text
可接受积压任务数 ≈ 每秒处理能力 * 可接受等待秒数

例如线程池每秒能处理 100 个任务,业务最多允许排队 3 秒,那么队列可以从 300 左右开始压测。最终参数必须根据压测和线上监控调整。

线程池隔离

不要所有业务共用一个大线程池。

mermaid
flowchart TD
    A["订单服务"] --> B["订单查询线程池"]
    A --> C["支付查询线程池"]
    A --> D["物流查询线程池"]
    A --> E["异步日志线程池"]
    C --> F["支付接口慢只影响支付查询"]

线程池隔离的意义是防止一个慢下游拖死所有业务。支付接口慢,就不应该把订单查询、库存查询、日志写入全部占满。

必须监控的指标

指标含义风险信号
poolSize当前线程数持续接近最大线程数
activeCount正在执行任务的线程数长时间等于最大线程数
queueSize队列积压任务数持续上涨
completedTaskCount已完成任务数增长变慢
largestPoolSize历史最大线程数经常打到上限
拒绝次数被拒绝任务数量出现突增
任务耗时单任务执行时间P95/P99 上升

监控 Demo:

java
public class ThreadPoolMonitor {
    public static void print(ThreadPoolExecutor pool) {
        System.out.println("poolSize=" + pool.getPoolSize());
        System.out.println("active=" + pool.getActiveCount());
        System.out.println("queue=" + pool.getQueue().size());
        System.out.println("completed=" + pool.getCompletedTaskCount());
        System.out.println("largest=" + pool.getLargestPoolSize());
    }
}

真实项目里应该把这些指标上报到 Prometheus、Micrometer、Actuator 或日志平台,而不是只打印到控制台。

线上排查流程

mermaid
flowchart TD
    A["接口变慢或任务积压"] --> B["看线程池 active 和 queue"]
    B --> C{"active 是否接近最大线程数"}
    C -- "否" --> D["排查提交速度、调度、锁等待"]
    C -- "是" --> E{"queue 是否持续上涨"}
    E -- "是" --> F["线程池处理能力不足或下游慢"]
    E -- "否" --> G["可能是瞬时峰值"]
    F --> H["jstack 查看线程在等什么"]
    H --> I["看数据库、Redis、HTTP 超时和慢日志"]

如果线程都卡在数据库连接获取,应该调整数据库连接池、SQL、限流,而不是简单把线程池加大。

商业场景:采集线程池

采集任务通常是 IO 密集型,但目标系统承受能力有限。线程池配置要控制并发,不能越快越好。

java
ThreadPoolExecutor collectPool = new ThreadPoolExecutor(
        10,
        20,
        60,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<Runnable>(1000),
        r -> new Thread(r, "collect-worker-" + System.nanoTime()),
        new ThreadPoolExecutor.CallerRunsPolicy()
);

这个配置的含义:

  1. 平时最多 10 个采集任务并发。
  2. 峰值最多 20 个,避免打爆目标系统。
  3. 队列最多积压 1000 个,超过后反压提交方。
  4. 需要额外配合超时、重试、幂等和失败记录。

面试标准回答

问题:线程池参数怎么设置?

标准回答:

线程池参数要先区分 CPU 密集型还是 IO 密集型。CPU 密集型线程数通常接近 CPU 核心数;IO 密集型可以适当大一些,但必须受数据库连接池、HTTP 连接池、下游接口能力和超时时间约束。队列要有界,拒绝策略要明确,核心业务不能静默丢弃。最终参数不能只靠公式,需要结合压测和线上监控持续调整。

本章小结

线程池参数估算不是数学题,而是容量设计。线程数、队列长度、下游容量、超时、拒绝策略和监控必须一起考虑。没有监控的线程池,迟早会变成线上黑盒。