线程数设置多少合适?公式为什么不靠谱?
结论先行:经典公式只是起点,不是答案。CPU 密集任务取 N+1,IO 密集任务取 N × (1 + 等待时间/计算时间);真正合适的线程数只能靠压测找拐点,因为"等待占比"随下游 RT 实时变化。
1. 两个公式
| 任务类型 | 公式 | 8 核机器示例 | 直觉解释 |
|---|---|---|---|
| CPU 密集 | N + 1 | 9 | 每个核一个线程刚好跑满,多 1 个顶上偶发的页缺失 |
| IO 密集 | N × (1 + W/C) | 等待:计算 = 9:1 → 80 | 线程阻塞时把 CPU 让给别人,等待越久越需要更多线程 |
其中 N 是核数,W 是等待时间(DB、RPC、磁盘),C 是计算时间。公式的来源是"让 CPU 的忙碌比例接近 100%"。
2. 为什么公式只能当起点
QPS
▲ ╭──── 拐点:再加线程,QPS 不再增长
│ ╭─╯
│ ╭─╯
│ ╭─╯
│ ╭─╯
│╭─╯
└────────────────────────────▶ 线程数
1 2 4 8 16 32 64 128
└── 收益区 ──┘└── 恶化区(切换 + 锁竞争 + 下游过载)──┘
三个让公式失准的现实因素:
- W/C 是动态的:下游变慢时等待时间从 10ms 涨到 500ms,公式算出的线程数瞬间翻几十倍,但此时加线程只会压垮下游;
- 瓶颈往往不在 CPU:数据库连接池只有 20 个连接,线程开 200 个也只是在连接池上排队;
- 上下文切换有成本:线程数超过某个点后,切换开销会吃掉全部收益(见"上下文切换"一篇)。
3. 用压测定参
// 先按公式给初值,再逐档加压观察
int cores = Runtime.getRuntime().availableProcessors();
double ratio = estimateWaitComputeRatio(); // 从监控里估:RT / CPU 时间
int init = (int) Math.ceil(cores * (1 + ratio));
ThreadPoolExecutor pool = new ThreadPoolExecutor(
init, init, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(2000),
new ThreadFactory() {
private int i = 0;
public Thread newThread(Runnable r) {
return new Thread(r, "biz-" + (i++));
}
},
new ThreadPoolExecutor.CallerRunsPolicy());
压测时固定 QPS 逐档提升线程数,记录四组数据:
| 线程数 | QPS | P99 (ms) | CPU 使用率 | 判断 |
|---|---|---|---|---|
| 8 | 1200 | 45 | 55% | 还有余量 |
| 16 | 2100 | 60 | 78% | 收益明显 |
| 32 | 2350 | 120 | 92% | 接近拐点 |
| 64 | 2100 | 380 | 96% | 已过拐点,恶化 |
结论取"QPS 最高且 P99 可接受"的前一档(上表是 32 或 16),而不是盲目取最大值。
4. 另一种算法:Little 定律
系统稳定时:并发数 = QPS × 平均响应时间。
200 QPS × 50ms = 10 个并发
这个式子给出的是"需要多少并发处理能力",比"多少线程"更本质:如果 10 个并发就够,线程数配到 20 已经有两倍余量;配到 200 只是在制造排队。
5. 常见追问
追问一:为什么 CPU 密集是 N+1 而不是 N?
因为线程偶尔会因为页缺失、GC 安全点等原因短暂停顿,多一个线程能填补这个空隙。再多就纯属浪费——核就那么多。
追问二:IO 密集能无限加线程吗?
不能。线程数受三样东西约束:内存(每线程约 1MB 栈)、下游承载能力(连接池、DB)、上下文切换成本。经验上 IO 密集取 2N ~ 4N 已经很激进,超过这个量级应该考虑异步/协程。
追问三:容器里 availableProcessors() 准吗?
JDK 8u191 之后支持容器感知(UseContainerSupport),会读 cgroup 的 CPU quota。但老版本或在 K8s 里用 CPU limit 而非 request 时,可能读到宿主机核数,导致线程数虚高。可以用 -XX:ActiveProcessorCount=4 显式指定。
追问四:虚拟线程时代还需要这个公式吗?
不需要算线程数了,但仍需要算并发上限:用 Semaphore 限制同时访问下游的数量(比如最多 50 个并发查询),否则百万虚拟线程会把连接池和下游打爆。
小结
线程数 = 让 CPU 忙起来所需的最小值,再留一点余量。公式负责给初值,压测负责定终值,Little 定律负责告诉你"到底需要多少并发"——三者结合,比背一个 2N+1 靠谱得多。