线程池核心线程数-计算方式
线程池的工况一般分为三种场景:
计算密集型
需要大量的计算,对 CPU 高占用率,CPU Loading 90-100%,除开 CPU需要读/写I/O(硬盘/内存),但这些 I/O 只需要很短的时间就可以完成,更多的是 CPU 需要进行很多数据运算,数学运算,CPU Loading 很高的场景。
例如数据分析,数据流处理,此类程序运行的过程中,CPU占用率一般都很高。
假如在单核CPU情况下,线程池有6个线程,但是由于是单核CPU,所以同一时间只能运行一个线程,考虑到线程之间还有上下文切换的时间消耗,还不如单个线程执行高效。所以,单核 CPU 处理计算密集型程序,就不要使用多线程了。
假如是6个核心的CPU,设置6个线程数,理论上运行速度可以提升6倍(但实际上达不到,多线程之间有并发以及需要优化的地方)。每个线程都有 CPU 来运行,并不会发生等待 CPU 时间片的情况,也没有线程切换的开销。多核 CPU 处理计算密集型程序才合适,而且中间可能没有线程的上下文切换(一核心处理对应一线程,一般配置不会超过CPU数量+1)。
IO密集型
和计算密集型相反,更多的是去处理磁盘,网络上的IO,对CPU占用不高,更多的是 CPU 等待 IO处理响应的场景,在等待IO处理这段时间线程会被阻塞,CPU空闲。这样压力更多的是磁盘或者网络上的传输效率问题。(一般CPU核心数 * 2,这样设置线程数其实不够严谨),对于IO密集型有对应公式可以套用。例如我们Web应用程序的后台,其实更多的场景是去增删改查数据库或者缓存,很多的接口等待时间被花费在了磁盘,网络IO上,那么我们设置线程池时核心线程数就可以根据服务器分配的CPU资源进行公式套用。
《Java 并发编程实战》中的计算公式:
Nthreads =Ncpu∗Ucpu ∗ (1+ C/W )
Ncpu:CPU核心数
Ucpu:CPU利用率
C/W:等待时间/计算时间
虽然可以通过公式得出预期的线程数,但在真实的程序中,一般很难获得准确的等待时间和计算时间,因为程序很复杂,不只是"计算"。一段代码中会有很多的内存读写,计算,I/O 等复合操作,精确的获取这两个指标很难,所以光靠公式计算线程数过于理想化,不过我们可以再根据压力测试进行核心线程数的具体调整,到达最高的效率预期。
混合型
有一些应用程序,计算处理数据的同时又需要进行磁盘或者网络数据传输,对于这样的应用程序我们如何去配置它的线程池,从而获取到最高的性能呢,一般来说会根据服务器的CPU核心数进行拆分,创建两个线程池,一个线程池对应计算资源,另一个线程池对应我们的IO部分,这样来配置是比较合理的。不过在网上也有其他方案:
核心线程数 =(线程等待时间/ 线程CPU时间 +1 )* CPU核心数
真实程序中的线程数
那么在实际的程序中,或者说一些Java的业务系统中,线程数(线程池大小)规划多少合适呢?
先说结论:没有固定答案,先设定预期,比如我期望的CPU利用率在多少,负载在多少,GC频率多少之类的指标后,先通过公式设置核心线程数,再通过测试不断的调整到一个合理的线程数。
总结:对于线程池设置核心线程数大小,可以根据公式套出太概值,但这个受干扰因素特别多,特别是服务器计算资源争夺问题,如果是直接在服务器上跑我们的应用程序,没有其他程序的干扰我们就通过不断的压测和调整找到合适的线程数,但目前很多应用程序已经容器化,在K8S环境中很多容器都是分布在不同的worker节点上,一个worker节点可能运行着很多容器,如果不对其他容器做资源限制,很容易发生计算资源争夺,那么就需要把每个容器的CPU,内存,硬盘,网络等资源的限制做好,不然很有可能运行效率远低于预期(被其他容器占用了计算资源)。所以还是得根据具体场景做具体压测,最终得出预期值。