← Java

Java 线程池的容量设计:把背压留在可控的位置

从线程、队列和拒绝策略三个边界出发,解释突发流量如何变成延迟与内存,并给出过载验收方法。

Read in English →

工程学习与设计笔记。示例使用合成场景;适用版本与参考资料见正文。

适用范围:Java 17 的平台线程与 ThreadPoolExecutor。本文不讨论 Java 21 虚拟线程;容量数字仅用于解释,不是推荐配置。

线程池最重要的作用,是把系统愿意同时承担的工作量说清楚。当下游处理速度低于流入速度时,积压一定会出现。区别只是它发生在内存队列、调用方、消息队列,还是表现为明确的拒绝。

队列不是吞吐量

假设某外部调用平均占用工作线程 20 毫秒,平均到达速率为每秒 500 次,稳定情况下平均在途需求大约是 10 个。这个估算只能作为起点:它没有覆盖长尾、重试、突发和下游容量,更不能由此推导“10 个线程足够”。

当下游抖动到 200 毫秒时,同样速率需要更多并发才能维持吞吐;但增加线程可能进一步压垮下游。如果仍然无条件接收,队列会吸收差额,排队时间逐渐超过请求剩余期限,系统开始认真处理已经没有用户等待的任务。

因此容量设计至少需要两个预算:同时执行多少任务,最多让多少任务等待。还应在任务入队时记录时间,在执行前检查剩余期限,及时丢弃已经失效且允许放弃的工作。

一个显式的起点

下面的片段放在服务初始化位置,所用类型来自 java.util.concurrent。线程数与队列容量需要通过自身负载验证:

ThreadPoolExecutor ioPool = new ThreadPoolExecutor(
    16, 16,
    0L, TimeUnit.MILLISECONDS,
    new ArrayBlockingQueue<>(256),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.AbortPolicy()
);

try {
    ioPool.execute(task);
} catch (RejectedExecutionException overloaded) {
    // 在接入层记录拒绝,并向调用方返回可识别的过载结果。
    // 不要在这里无限重试,也不要静默丢失可靠消息。
    throw overloaded;
}

ThreadPoolExecutor 的扩容受队列行为影响:超过核心线程数后通常先尝试入队,队列满后才尝试增加线程;无界队列可能让 maximumPoolSize 失去预期的限流作用。准确行为见 ThreadPoolExecutor 文档 。

ArrayBlockingQueue 给出固定的排队任务数上限,但“用了阻塞队列”不等于线程池提交时一定阻塞。线程池的提交路径与直接调用队列 put 不同。队列本身的语义见 ArrayBlockingQueue 。

拒绝策略应该匹配入口

HTTP 接口可以把过载返回为调用方能识别的状态,并约定重试规则。可靠消息消费则应暂停拉取或降低并发;拒绝后直接确认消息,相当于把容量问题变成数据丢失。补偿队列也必须有容量、保留期和观测,不能只是把无限积压搬个地方。

CallerRunsPolicy 常被用作背压:让提交者自己执行任务,从而降低提交速率。但它会占用调用线程;如果这个线程承担事件循环、心跳或消费协调工作,影响可能扩大。它在执行器关闭时还会丢弃被拒绝任务,语义见 CallerRunsPolicy 。因此不能把它当成所有入口都安全的默认选项。

对于多个下游,按依赖隔离执行资源通常更容易控制故障范围。一个慢查询服务不应耗尽发送心跳或处理关键响应的线程。不过,隔离也会产生空闲容量无法互借的问题,需要结合流量与优先级取舍。

监控要把“等待”单独量出来

只看线程数和 CPU 会漏掉大量问题。建议至少记录任务排队时长、实际执行时长、活跃线程数、队列长度、拒绝数、过期任务数,以及每个下游的耗时与错误率。端到端耗时上升时,先区分时间花在排队还是执行。

队列内存也不能只用“任务个数 × 请求体大小”估算。闭包可能引用大对象、上下文、缓冲区或整批数据;应检查对象保留关系。把队列扩大十倍,可能同时放大延迟尾部和堆占用。

过载测试比均匀压测更有价值

验证至少包含三段:稳定流量、突然提升输入、将下游延迟人为拉高后恢复。观察系统是否在预定边界拒绝、是否能恢复、拒绝是否被上游放大成重试风暴。再模拟进程关闭,确认不再接受新任务,已接收任务的结束与回收策略符合业务要求。

一个合格结果并不是“任何输入都不报错”,而是:容量以内延迟可控,容量以外失败明确,下游恢复后积压能清空,可靠数据有明确的重放路径。

References

← 继续阅读专题回到顶部 ↑