WMX · 随记
·Java· 5 分钟阅读

线程池不是越大越好:队列、拒绝与背压

从 ThreadPoolExecutor 的任务接收顺序入手,理解有界队列为什么是系统容量的一部分。

线程池容量示意:接收任务、有界队列、工作线程;资源耗尽时需要明确的拒绝策略。
线程池容量示意:接收任务、有界队列、工作线程;资源耗尽时需要明确的拒绝策略。 点击图片查看大图 ↗

请求变慢时,很容易想把线程数调大。可如果真正的瓶颈是数据库连接、下游配额或 CPU,更多线程可能只是制造更多等待。线程池首先是资源管理工具,其次才是并发执行的便利接口。

先看清任务接收的顺序

对一个运行中的 ThreadPoolExecutor,可以先用四个步骤理解其核心接收逻辑:工作线程少于 corePoolSize 时尝试创建线程;否则尝试入队;队列无法接收时,才尝试增加线程直到 maximumPoolSize;仍然无法接收就执行拒绝策略。实际实现还会重新检查运行状态,处理关闭等并发变化。

因此,使用几乎不会满的无界队列时,maximumPoolSize 往往不能像想象中那样发挥扩容作用,而排队时间和内存占用可能持续增长。

一个明确限制容量的配置

下面的数字仅用于演示配置关系,不是面向所有服务的推荐参数。示例适用于 Java 17 及以上。
Java24 行 · 可横向滚动
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class PoolDemo {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
            4, 8, 30, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(32),
            new ThreadPoolExecutor.AbortPolicy()
        );
        try {
            for (int i = 0; i < 10; i++) {
                final int job = i;
                pool.execute(() -> System.out.println("job " + job));
            }
        } finally {
            pool.shutdown();
            if (!pool.awaitTermination(10, TimeUnit.SECONDS)) {
                pool.shutdownNow();
            }
        }
    }
}

这里队列最多暂存 32 个任务。AbortPolicy 在饱和或关闭时抛出 RejectedExecutionException。上层需要把这个信号转成明确行为,例如快速失败、可控重试或返回“系统繁忙”,而不是无上限地重新提交。生产代码还需要正确处理中断和应用关闭生命周期。

CallerRunsPolicy 为什么不能到处套用

CallerRunsPolicy 在池仍运行时会让提交任务的线程执行任务,让生产者慢下来。这有时形成背压,但也意味着调用线程会阻塞。如果提交方是事件循环线程,或者持有重要的锁,这可能放大延迟甚至引发其他问题。线程池关闭时,该策略会丢弃任务,也需要纳入业务语义。

比线程数更值得盯住的指标

  • 排队时长:任务提交到开始执行之间等了多久。
  • 执行时长与下游等待:线程忙着计算,还是在等网络和连接?
  • 队列深度、拒绝次数与超时率:容量不足何时开始出现?
  • 端到端延迟分位数:平均值正常时,尾部请求可能已经不可接受。

调参时固定输入负载,逐项改变配置,再观察吞吐、延迟和资源占用。别把一次本地跑得更快,直接当成生产环境的容量结论。

继续阅读

← 返回文章归档