跳到主要内容

线程池核心线程数-计算方式

· 阅读需 5 分钟

线程池的核心线程数该设多少,取决于程序的工况。一般分为三种场景:计算密集型、IO 密集型和混合型,下面分别来看。

计算密集型

需要大量的计算,对 CPU 占用率高,CPU Loading 90-100%。除开 CPU 需要读/写 I/O(硬盘/内存),但这些 I/O 只需要很短的时间就可以完成,更多的是 CPU 在做大量数据运算、数学运算。例如数据分析、数据流处理,此类程序运行过程中 CPU 占用率一般都很高。

假如在单核 CPU 情况下,线程池有 6 个线程,但由于是单核,同一时间只能运行一个线程,考虑到线程之间还有上下文切换的时间消耗,还不如单个线程执行高效。所以,单核 CPU 处理计算密集型程序,就不要使用多线程了。

假如是 6 个核心的 CPU,设置 6 个线程数,理论上运行速度可以提升 6 倍(实际上达不到,多线程之间有并发以及需要优化的地方)。每个线程都有 CPU 来运行,不会发生等待 CPU 时间片的情况,也没有线程切换的开销。多核 CPU 处理计算密集型程序才合适,而且中间可能没有线程的上下文切换(一核心对应一线程,一般配置不会超过 CPU 数量 + 1)。

IO密集型

和计算密集型相反,更多的是处理磁盘、网络上的 IO,对 CPU 占用不高,大部分时间是 CPU 在等待 IO 响应,等待期间线程被阻塞、CPU 空闲,压力更多在磁盘或网络的传输效率上。这种场景下简单地按 CPU 核心数 × 2 来设置线程数其实不够严谨,IO 密集型有对应的公式可以套用。例如 Web 应用的后台,更多的场景是增删改查数据库或缓存,很多接口的耗时都花在了磁盘、网络 IO 上,那么设置线程池核心线程数时就可以根据服务器分配的 CPU 资源套用公式。

《Java 并发编程实战》中的计算公式:

Nthreads = Ncpu × Ucpu × (1 + W/C)

  • Ncpu:CPU 核心数
  • Ucpu:CPU 利用率
  • W/C:等待时间 / 计算时间

虽然可以通过公式得出预期的线程数,但在真实的程序中,一般很难获得准确的等待时间和计算时间,因为程序很复杂,不只是"计算"。一段代码中会有很多内存读写、计算、I/O 等复合操作,精确获取这两个指标很难,所以光靠公式计算线程数过于理想化。不过我们可以在此基础上,再通过压力测试对核心线程数做具体调整,达到最高的效率预期。

混合型

有一些应用程序,计算处理数据的同时又需要进行磁盘或网络的数据传输。对于这样的程序如何配置线程池才能获得最高性能?一般来说会根据服务器的 CPU 核心数进行拆分,创建两个线程池:一个线程池对应计算部分,另一个对应 IO 部分,这样配置比较合理。网上也有另一种方案:

核心线程数 = (线程等待时间 / 线程 CPU 时间 + 1) × CPU 核心数

真实程序中的线程数

那么在实际的程序中,或者说一些 Java 业务系统中,线程数(线程池大小)规划多少合适呢?

先说结论:没有固定答案。先设定预期,比如期望的 CPU 利用率是多少、负载多少、GC 频率多少之类的指标,然后通过公式设置核心线程数,再通过测试不断调整到一个合理的线程数。

公式只能套出一个大概值,实际受干扰的因素特别多,尤其是服务器计算资源的争夺。如果应用程序直接跑在服务器上、没有其他程序干扰,通过不断压测和调整就能找到合适的线程数。但目前很多应用已经容器化,在 K8S 环境中容器分布在不同的 worker 节点上,一个 worker 节点可能运行着很多容器,如果不对容器做资源限制,很容易发生计算资源争夺。这就需要把每个容器的 CPU、内存、硬盘、网络等资源限制做好,不然运行效率很可能远低于预期(计算资源被其他容器占走了)。

小结

线程池核心线程数没有放之四海皆准的数值:计算密集型贴着 CPU 核心数配置,IO 密集型可以用 Nthreads = Ncpu × Ucpu × (1 + W/C) 估算,混合型则考虑按计算和 IO 拆分成两个线程池。公式给出的只是起点,等待时间与计算时间在真实程序里很难精确测量,还要叠加容器化环境下的资源争夺问题。所以合理的做法是:先按公式定一个初始值,设定好 CPU 利用率、负载等预期指标,再通过压测逐步调整到符合预期为止。

评论 / COMMENTS