多线程线程数设置多少合适

前沿

大家都用过线程池,但是线程池数量设置为多少比较合理呢?

线程数的设置的最主要的目的是为了充分并合理地使用 CPU 和内存等资源,从而最大限度地提高程序的性能,因此让我们一起去探索吧!

首先要考虑到 CPU 核心数,那么在 Java 中如何获取核心线程数?

可以使用 Runtime.getRuntime().availableProcessor() 方法来获取(可能不准确,作为参考)
或者直接去服务器查看

温故为什么使用线程

场景

如果有两个任务需要处理,一个任务A,一个任务B

方案一:一个线程执行任务A和B,A执行完后,执行B
方案二:两个线程A和B去执行任务A 和 B,同时进行

哪个方案会快点?应该很多人会回答,肯定是方案二啊,多线程并行去处理任务A和B,肯定快啊。是这样吗?回答这个问题之前,先带着大家去回顾梳理一下。

线程执行

线程的执行,是由CPU进行调度的,一个CPU在同一时刻只会执行一个线程,我们看上去的线程A 和 线程B并发执行。

为了让用户感觉这些任务正在同时进行,操作系统利用了时间片轮转的方式,CPU给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务。任务的状态保存及再加载,这段过程就叫做上下文切换。

上下文切换过程是需要时间的;现在我们来看一下上面的问题,小伙伴们再看一下是哪个方案快呢?是不是有些小伙伴们会说方案一,因为不需要线程切换;方案二需要来回切换这两个线程,耗时会多点。

小伙伴们心中此时是不是会有疑惑,那为什么会有多线程?先不急,再往下看。

为什么要使用多线程

小伙伴想想在我们真实业务中,我们是什么流程?
在这里插入图片描述

上图的流程:

1、先发起网络请求

2、Web服务器解析请求

3、请求后端的数据库获取数据

4、获取数据后,进行处理

5、把处理结果放回给用户

这个是我们处理业务的时候,常规的请求流程;我们看一下整个过程涉及到什么计算机处理。

1、网络请求----->网络IO

2、解析请求----->CPU

3、请求数据库----->网络IO

4、MySQL查询数据----->磁盘IO

5、MySQL返回数据----->网络IO

6、数据处理----->CPU

7、返回数据给用户----->网络IO

讲到这里,小伙伴们是不是感觉又不乱了,在真实业务中我们不单单会涉及CPU计算,还有网络IO和磁盘IO处理,这些处理是非常耗时的。如果一个线程整个流程是上图的流程,真正涉及到CPU的只有2个节点,其他的节点都是IO处理,那么线程在做IO处理的时候,CPU就空闲出来了,CPU的利用率就不高。

小伙伴们现在知道多线程的用处了吧,对,就是为了提升CPU利用率。

提升QPS/TPS

衡量系统性能如何,主要指标系统的(QPS/TPS)

QPS/TPS:每秒能够处理请求/事务的数量

并发数:系统同时处理的请求/事务的数量

响应时间:就是平均处理一个请求/事务需要时长

QPS/TPS = 并发数/响应时间

上面公式代表并发数越大,QPS就越大;所以很多人就会以为调大线程池,并发数就会大,也会提升QPS,所以才会出现一开始前言所说的,大多数人的误区。

其实QPS还跟响应时间成反比,响应时间越大,QPS就会越小。

虽然并发数调大了,就会提升QPS,但线程数也会影响响应时间,因为上面我们也提到了上下文切换的问题,那怎么设置线程数的呢?

如何设置线程数

那我们如何分配线程?我们提供一个公式:

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

备注:这个公式也是前辈们分享的,当然之前看了淘宝前台系统优化实践的文章,和上面的公式很类似,不过在CPU数目那边,他们更细化了,上面的公式只是参考。不过不管什么公式,最终还是在生产环境中运行后,再优化调整。

我们继续上面的任务,我们的服务器CPU核数为4核,一个任务线程cpu耗时为20ms,线程等待(网络IO、磁盘IO)耗时80ms,那最佳线程数目:( 80 + 20 )/20 * 4 = 20。也就是设置20个线程数最佳。

从这个公式上面我们就得出,线程的等待时间越大,线程数就要设置越大,这个正好符合我们上面的分析,可提升CPU利用率。那从另一个角度上面说,线程数设置多大,是根据我们自身的业务的,需要自己去压力测试,设置一个合理的数值。

基础常规标准

在确认了核心数后,再去判断是 CPU 密集型任务还是 IO 密集型任务:
CPU 密集型任务:
比如像加解密,压缩、计算等一系列需要大量耗费 CPU 资源的任务,大部分场景下都是纯 CPU 计算。
IO 密集型任务:
比如像 MySQL 数据库、文件的读写、网络通信等任务,这类任务不会特别消耗 CPU 资源,但是 IO 操作比较耗时,会占用比较多时间。

1、CPU密集型:操作内存处理的业务,一般线程数设置为:CPU核数 + 1 或者 CPU核数*2。核数为4的话,一般设置 5 或 8

2、IO密集型:文件操作,网络操作,数据库操作,一般线程设置为:cpu核数 / (1-0.9),核数为4的话,一般设置 40
在知道如何判断任务的类别后,让我们分两个场景进行讨论:

CPU 密集型任务

对于 CPU 密集型计算,多线程本质上是提升多核 CPU 的利用率,所以对于一个 8 核的 CPU,每个核一个线程,理论上创建 8 个线程就可以了。

如果设置过多的线程数,实际上并不会起到很好的效果。此时假设我们设置的线程数量是 CPU 核心数的 2 倍,因为计算任务非常重,会占用大量的 CPU 资源,所以这时 CPU 的每个核心工作基本都是满负荷的,而我们又设置了过多的线程,每个线程都想去利用 CPU 资源来执行自己的任务,这就会造成不必要的上下文切换,此时线程数的增多并没有让性能提升,反而由于线程数量过多会导致性能下降。

因此,对于 CPU 密集型的计算场景,理论上线程的数量 = CPU 核数就是最合适的,不过通常把线程的数量设置为CPU 核数 +1,会实现最优的利用率。即使当密集型的线程由于偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程也能确保 CPU 的时钟周期不会被浪费,从而保证 CPU 的利用率。

如下图就是在一个 8 核 CPU 的电脑上,通过修改线程数来测试对 CPU 密集型任务(素数计算)的性能影响。
在这里插入图片描述

可以看到线程数小于 8 时,性能是很差的,在线程数多于处理器核心数对性能的提升也很小,因此可以验证公式还是具有一定适用性的。

除此之外,我们最好还要同时考虑在同一台机器上还有哪些其他会占用过多 CPU 资源的程序在运行,然后对资源使用做整体的平衡。

IO 密集型任务

对于 IO 密集型任务最大线程数一般会大于 CPU 核心数很多倍,因为 IO 读写速度相比于 CPU 的速度而言是比较慢的,如果我们设置过少的线程数,就可能导致 CPU 资源的浪费。而如果我们设置更多的线程数,那么当一部分线程正在等待 IO 的时候,它们此时并不需要 CPU 来计算,那么另外的线程便可以利用 CPU 去执行其他的任务,互不影响,这样的话在任务队列中等待的任务就会减少,可以更好地利用资源。

对于 IO 密集型计算场景,最佳的线程数是与程序中 CPU 计算和 IO 操作的耗时比相关的,《Java并发编程实战》的作者 Brain Goetz 推荐的计算方法如下:

线程数 = CPU 核心数 * (1 + IO 耗时/ CPU 耗时)

通过这个公式,我们可以计算出一个合理的线程数量,如果任务的平均等待时间长,线程数就随之增加,而如果平均工作时间长,也就是对于我们上面的 CPU 密集型任务,线程数就随之减少。可以采用 APM 工具统计到每个方法的耗时,便于计算 IO 耗时和 CPU 耗时。

在这里引用Java并发编程实战中的图,方便大家更容易理解:
在这里插入图片描述

还有一派的计算方式是《Java虚拟机并发编程》中提出的:

线程数 = CPU 核心数 / (1 - 阻塞系数)

其中计算密集型阻塞系数为 0,IO 密集型阻塞系数接近 1,一般认为在 0.8 ~ 0.9 之间。比如 8 核 CPU,按照公式就是 2 / ( 1 - 0.9 ) = 20 个线程数
在这里插入图片描述

上图是 IO 密集型任务的一个测试,是在双核处理器上开不同的线程数(从 1 到 40)来测试对程序性能的影响,可以看到线程池数量达到 20 之后,曲线逐渐水平,说明开再多的线程对程序的性能提升也毫无帮助。

太少的线程数会使得程序整体性能降低,而过多的线程也会消耗内存等其他资源,所以如果想要更准确的话,可以进行压测,监控 JVM 的线程情况以及 CPU 的负载情况,根据实际情况衡量应该创建的线程数,合理并充分利用资源。

同时,有很多线程池的应用,比如 Tomcat、Redis、Jdbc 等,每个应用设置的线程数也是不同的,比如 Tomcat 为流量入口,那么线程数的设置可能就要比其他应用要大。

总结

通过对线程数设置的探究,我们可以得知线程数的设置首先和 CPU 核心数有莫大关联,除此之外,我们需要根据任务类型的不同选择对应的策略,具体的怎么设置要根据业务上需要/服务器的环境/QPS/TPS等指标等等有关系。
线程的平均工作时间所占比例越高,就需要越少的线程;线程的平均等待时间所占比例越高,就需要越多的线程;增强cpu的使用率。
针对不同的程序,进行对应的实际测试就可以得到最合适的选择。

猜你喜欢

转载自blog.csdn.net/fclwd/article/details/107032165