线程数设置多少合适?公式为什么不靠谱?

#Java#面试八股

线程数设置多少合适?公式为什么不靠谱?

结论先行:经典公式只是起点,不是答案。CPU 密集任务取 N+1,IO 密集任务取 N × (1 + 等待时间/计算时间);真正合适的线程数只能靠压测找拐点,因为"等待占比"随下游 RT 实时变化。

1. 两个公式

任务类型公式8 核机器示例直觉解释
CPU 密集N + 19每个核一个线程刚好跑满,多 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
      └── 收益区 ──┘└── 恶化区(切换 + 锁竞争 + 下游过载)──┘

三个让公式失准的现实因素:

  1. W/C 是动态的:下游变慢时等待时间从 10ms 涨到 500ms,公式算出的线程数瞬间翻几十倍,但此时加线程只会压垮下游;
  2. 瓶颈往往不在 CPU:数据库连接池只有 20 个连接,线程开 200 个也只是在连接池上排队;
  3. 上下文切换有成本:线程数超过某个点后,切换开销会吃掉全部收益(见"上下文切换"一篇)。

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 逐档提升线程数,记录四组数据:

线程数QPSP99 (ms)CPU 使用率判断
812004555%还有余量
1621006078%收益明显
32235012092%接近拐点
64210038096%已过拐点,恶化

结论取"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 靠谱得多。

笔记加载中…