当前位置: 首页 > 科技观察

请不要再担心线程池的大小!

时间:2023-03-20 23:36:23 科技观察

图片来自Pexels。很多人可能看过一个设置线程数的理论:CPU密集型程序:核心数+1I/O密集型程序:核心数*2不不不,真的有人按照这个理论来规划线程数吗?01线程数和CPU利用率的小测试抛开一些操作系统和计算机原理,先说一个基础理论(不怕严谨,只是为了方便理解):一个CPU核心只能执行单位时间内一个线程的指令,所以理论上我只需要一个线程不停的执行指令就可以充分利用一个核的利用率。下面写一个无限循环空跑的例子来验证一下:测试环境:AMDRyzen53600,6-Core,12-Threads。publicclassCPUUtilizationTest{publicstaticvoidmain(String[]args){//无限循环,什么都不做while(true){}}}运行完这个例子,我们来看看当前的CPU利用率:从图中可以看出,我的3号核心的利用率已经满负荷运行。根据以上理论,我多开几个线程如何?publicclassCPUUtilizationTest{publicstaticvoidmain(String[]args){for(intj=0;j<6;j++){newThread(newRunnable(){@Overridepublicvoidrun(){while(true){}}}).start();}}}看看此时的CPU利用率,1/2/5/7/9/11个core的利用率已经全部跑完了:那如果12个线程呢,是不是所有core的利用率都跑满了呢?答案一定是肯定的:如果我继续把上面例子中的线程数增加到24个线程,会发生什么?从上图可以看出,CPU使用率和上一步一样,所有核心依然是100%。但是此时负载从11.x增加到22.x,说明此时CPU比较忙,线程的任务不能及时执行。Loadaverage解释参考:https://scoutapm.com/blog/understanding-load-averages现代CPU基本上都是多核的,比如我这里测试的AMD3600,6核12线程(超线程),我们可以简单的认为它是一个12核的CPU。那么我的CPU可以同时做12件事情,互不干扰。如果要执行的线程数大于核心数,则需要操作系统进行调度。操作系统为每个线程分配CPU时间片资源,然后不断切换,达到“并行”执行的效果。但这真的更快吗?从上面的例子可以看出,一个线程可以填满一个核的利用率。如果每个线程都非常“霸道”,不断地执行指令,不给CPU空闲时间,同时执行的线程数大于CPU的核心数,就会导致操作系统执行更频繁的执行切换线程,保证每个线程都能得到执行。但是切换是有代价的,每次切换都会伴随寄存器数据更新、内存页表更新等操作。虽然与I/O操作相比,一次切换的开销微不足道,但如果线程过多,线程切换过于频繁,甚至单位时间内切换所花费的时间大于程序执行时间,就会导致过多的CPU资源。花费精力在上下文切换而不是执行程序上是不值得的。上面这个死循环运行的例子有点过于极端了,一般情况下不太可能有这样的程序。大多数程序在运行时都会有一些I/O操作,可能是读写文件、通过网络发送和接收消息等,这些I/O操作在进行时需要等待反馈。比如在网络上读写时,需要等待消息的发送或接收。在这个等待过程中,线程处于等待状态,CPU不工作。这时候操作系统会调度CPU去执行其他线程的指令,完美的利用了CPU的空闲时间,提高了CPU的利用率。上面的例子中,程序一直循环,什么都不做,CPU要不停地执行指令,几乎没有空闲时间。如果插入一段I/O操作,在I/O操作过程中CPU处于空闲状态,CPU利用率会发生什么变化?先看单线程下的结果:publicclassCPUUtilizationTest{publicstaticvoidmain(String[]args)throwsInterruptedException{for(intn=0;n<1;n++){newThread(newRunnable(){@Overridepublicvoidrun(){while(true){//1亿次空循环后,sleep50ms,模拟I/O等待,switchfor(inti=0;i<100_000_000l;i++){}try{Thread.sleep(50);}catch(InterruptedExceptione){e.printStackTrace();}}}}).start();}}}哇塞,唯一有用的利用率更高的9号核只有50%,已经比之前不休眠的100%低了一半。现在调整线程数为12,看到:单核的利用率大约是60,和刚才的单线程结果相差不大,CPU利用率还没有完全跑出来。现在将线程数增加到18个:此时,单核利用率接近100%。可见,当线程中有I/O等不占用CPU资源的操作时,操作系统可以调度CPU同时执行更多的线程。现在我们提高I/O事件的频率,将周期数减半,50_000_000,也就是18个线程:此时每个核心的利用率只有70%左右。02线程数与CPU利用率总结以上例子只是辅助,为了更好的理解线程数/程序行为/CPU状态之间的关系。简单总结一下:一个极限线程(连续执行“计算”操作时)可以充分利用单核的利用率,而多核CPU最多只能执行与计算次数相等的“极限”线程数核心同时。如果每个线程都这么“极端”,同时执行的线程数超过核心数,就会造成不必要的切换,导致负载过大,只会让执行变慢。I/O等暂停操作时,CPU空闲,操作系统调度CPU去执行其他线程,可以提高CPU利用率,同时执行更多线程。I/O事件的频率越高,或者等待/暂停时间越长,CPU的空闲时间越长,利用率越低,操作系统可以调度CPU执行更多的线程。03线程数规划公式前面的铺垫是为了帮助理解,现在我们来看书上的定义。《Java 并发编程实战》介绍了一个计算线程数的公式:如果想让程序运行到CPU的目标利用率,需要的线程数的公式是:公式很清楚,我们试试上面的例子现在。如果我预计目标利用率是90%(多核90),那么需要的线程数是:核数12*利用率0.9*(1+50(睡眠时间)/50(循环50_000_000耗时))≈22。现在调整线程数为22,看看结果:现在CPU利用率在80+左右,接近预期。由于线程数过多,有一些上下文切换的开销,加上测试用例不够严谨,所以实际利用率较低也是正常的。换个公式,CPU利用率也可以通过线程数来计算:线程数22/(核心数12*(1+50(休眠时间)/50(循环50_000_000耗时)))≈0.9。公式虽好,但在实际程序中,一般很难得到准确的等待时间和计算时间,因为程序复杂,不仅仅是“计算”。一段代码会有很多内存读写、计算、I/O等复杂操作,很难准确获取这两个指标,所以只通过公式计算线程数太理想了.04实际程序中的线程数那么在实际程序中,或者在一些Java业务系统中,规划多少线程数(线程池大小)合适呢?先说结论:没有固定的答案,先设定预期,比如我的预期CPU利用率、负载、GC频率等指标,通过测试不断调整到合理的线程数。比如一个普通的,基于SpringBoot的业务系统,默认的Tomcat容器+HikariCP连接池+G1收集器,如果此时项目也需要业务场景的多线程(或线程池)异步执行业务流程/平行线。这时候如果我按照上面的公式来规划线程数,误差肯定会很大。因为此时这台主机上已经有很多正在运行的线程,Tomcat有自己的线程池,HikariCP也有自己的后台线程,JVM也有一些编译线程,甚至G1也有自己的后台线程。这些线程同样运行在当前进程和当前主机上,同样会占用CPU资源。因此,在环境的干扰下,单靠公式很难准确规划出线程数,必须通过测试来验证。流程大致如下:分析当前主机是否有其他进程干扰。分析当前JVM进程上是否有其他正在运行或可能正在运行的线程。设定目标,目标CPU利用率:我可以容忍我的CPU达到多少?TargetGCfrequency/pausetime:多线程执行后,GC频率会增加,可以容忍的最大频率是多少,每次pause时间多长?执行效率:比如在批处理中,我需要在单位时间内开多少个线程才能及时处理完...整理一下链接的关键点,看看有没有卡点,因为如果线程过多,链路上部分节点资源有限可能会导致大量线程等待资源(如三方接口限流,连接池数量受限,中间件压力太大高支持等)。不断增加/减少测试的线程数,按照最高要求进行测试,最终得到“满足要求”的线程数。还有更多!不同场景下线程数的概念也不同:Tomcat中的maxThreads在BlockingI/O和No-BlockingI/O下是不一样的。Dubbo默认还是单连接,也有I/O线程(池)和业务线程(池)的区别。I/O线程一般不会成为瓶颈,不需要太多,但是业务线程很容易成为瓶颈。Redis6.0之后也是多线程的,但只是I/O多线程,“业务”处理还是单线程的。所以,不用担心要设置多少个线程。没有标准答案,必须结合场景,结合目标,通过测试找到最合适的线程数。可能有同学会有疑惑:“我们的系统没有任何压力,不需要这么合适的线程数,只是一个简单的异步场景,不影响系统的其他功能。”这是正常的。许多内部业务系统不需要任何性能。稳定好用,满足要求就够了。那么我推荐的线程数是:CPU核心数。05附录Java获取CPU核心数:Runtime.getRuntime().availableProcessors()//获取逻辑核心数,比如6核12线程,则返回12Linux获取CPU核心数:#Totalnumberofcores=物理CPU数X每个物理CPU的核心数#逻辑CPU总数=物理CPU数X每个物理CPU的核心数X超线程数#查看物理CPU数cat/proc/cpuinfo|grep"physicalid”|排序|uniq|wc-l#查看每个物理CPU的核心数(即核心数)cat/proc/cpuinfo|grep"cpucores"|uniq#查看逻辑CPU数cat/proc/cpuinfo|grep"processor》|wc-l作者:孔武编辑:陶家龙来源:juejin.cn/post/6948034657321484318