Java并发10:线程数量


Java并发10:线程数量

为什么使用多线程

提升性能

如何度量性能

延迟吞吐量

延迟:发出请求到收到响的时间;延迟越短,程序执行得越快,性能越好。

吞吐量:单位时间内能处理请求的数量;吞吐量越大,程序能处理的请求越多,性能越好。

两个指标内部有一定的联系(同等条件下,延迟越短,吞吐量越大),但是由于它们隶属不同维度(一个是时间维度,一个是空间维度),不能互相转换。

如何提高性能

降低延迟&提升吞吐量

多线程应用场景

提高性能方向:一是优化算法,二是将硬件性能发挥极致。前者算法范畴,后者与并发编程有关。

实际上操作系统已经解决了硬件的利用率问题:例如操作系统已经解决了磁盘和网卡的利用率问题,利用中断机制还能避免 CPU 轮询 I/O 状态,也提升了 CPU 的利用率。

但操作系统解决硬件利用率问题的对象往往是单一的硬件设备,而并发程序往往需要 CPU 和 I/O 设备相互配合工作,需要解决 CPU 和 I/O 设备综合利用率的问题

所以利用多线程解决综合利用率问题。

综合利用率示例

前提:假设程序按CPU 计算和 I/O 操作交叉执行的方式运行, CPU 计算和 I/O 操作的耗时是 1:1。

单线程

一个线程,执行 CPU 计算,I/O 设备空闲;执行 I/O 操作,CPU 空闲。

CPU 的利用率和 I/O 设备的利用率为50%。

单线程执行示意图

双线程

两个线程,线程 A 执行 CPU 计算,线程 B 执行 I/O 操作;线程 A 执行 I/O 操作,线程 B 执行 CPU 计算。

这CPU 的利用率和 I/O 设备的利用率就为100%。

两个线程执行示意图

即:逆向思维:如果 CPU 和 I/O 设备的利用率都很低,那么可以尝试通过增加线程来提高吞吐量

单核&多核区别

单核CPU下,多线程用来平衡CPU与I/O操作,如上面的单线程示例。

但若多线程没有I/O操作,在单核CPU下反而会使性能降低,大家都在排队等计算。

多核CPU下,多线程纯CPU计算也可能提升性能。

多核CPU纯计算提升性能示例

计算1+2+3+…+100亿,4核CPU就可以4个线程同时执行。

A计算1-25亿,B计算25-50亿,C计算50-75亿,D计算75-100亿。

多核多线程执行示意图

创建合适的线程数

场景分类

  • I/O密集型计算

    I/O 操作执行的时间相对于 CPU 计算非常长

  • CPU 密集型计算

    大部分场景下都是纯 CPU 计算

CPU 密集型计算

对于该场景,多线程本质上是提升多核 CPU 的利用率。

理论上:对于一个 4 核的 CPU,每个核一个线程,CPU利用率就是100%。

工程上:线程的数量一般会设置为“CPU 核数 +1”。

当线程因为偶尔的内存页失效或其他原因导致阻塞时,额外的线程可以顶上,从而保证 CPU 的利用率。

I/O密集型计算

单核最佳线程数 = 1 +(I/O 耗时 / CPU 耗时)

三线程执行示意图

如图T(I/O) = 2T(CPU),即单位时间内做一次I/O的时间可以做两次CPU计算。

所以同一时间,2个线程做I/O,1个线程做CPU计算。

*多核最佳线程数 = CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]*

总结

CPU密集型:根据公式,硬件性能发挥到极致。

I/O密集型:不同的I/O操作场景下动态变化。

因此工程上,原则是将硬件的性能发挥到极致。

压测时,要重点关注 CPU、I/O 设备的利用率和性能指标(响应时间、吞吐量)之间的关系。

思考

有些经验上认为对于 I/O 密集型应用,最佳线程数应该为:2*CPU 的核数 + 1,是否合理?

从理论上来讲,这个经验值一定是靠不住的。

但是经验值对于很多“I/O 耗时 / CPU 耗时”不太容易确定的系统来说,却是一个很好的初始值

最佳线程数最终还是靠压测来确定的,实际工作中大家面临的系统,“I/O 耗时 / CPU 耗时”往往都大于 1,所以基本上都是在这个初始值的基础上增加。增加的过程中,应关注线程数是如何影响吞吐量和延迟的

一般来讲,随着线程数的增加,吞吐量会增加,延迟也会缓慢增加;但是当线程数增加到一定程度,吞吐量就会开始下降,延迟会迅速增加。这个时候基本上就是线程能够设置的最大值了。

实际工作中,不同的 I/O 模型对最佳线程数的影响非常大,例如Nginx 用的是非阻塞 I/O,采用的是多进程单线程结构,Nginx 本来是一个 I/O 密集型系统,但是最佳进程数设置的却是 CPU 的核数,完全参考的是 CPU 密集型的算法。所以理论还是要活学活用。

别人的总结

更多的精力其实应该放在算法的优化上,线程池的配置,按照经验配置一个,随时关注线程池大小对程序的影响即可,具体做法:可以为你的程序配置一个全局的线程池,需要异步执行的任务,扔到这个全局线程池处理,线程池大小按照经验设置,每隔一段时间打印一下线程池的利用率,做到心里有数。

看到过太多的代码,遇到要执行一个异步任务就创建一个线程池,导致整个程序的线程池大到爆,完全没必要。而且大多数时候,提高吞吐量可以通过使用缓存、优化业务逻辑、提前计算好等方式来处理,真没有必要太过于关注线程池大小怎么配置,如果小了就改大一点,大了改小一点就好,从老师本文的篇幅也可以看出来。

经验值不靠谱的另外一个原因,大多数情况下,一台服务器跑了很多程序,每个程序都有自己的线程池,那CPU如何分配?还是根据实际情况来确定比较好。

——CHEN川

文中三线程图假设io是并行的,恐怕并不合理,实际理论上真的是只要两倍线程就好,再多的线程也无法提高利用率,都会因为io阻塞。

——Evan

个人觉得公式话性能问题有些不妥,定性的io密集或者cpu密集很难在定量的维度上反应出性能瓶颈,而且公式上忽略了线程数增加带来的cpu消耗,性能优化还是要定量比较好,这样不会盲目,比如io已经成为了瓶颈,增加线程或许带来不了性能提升,这个时候是不是可以考虑用cpu换取带宽,压缩数据,或者逻辑上少发送一些。最后一个问题,我的答案是大部分应用环境是合理的,题目也说了是积累了一些调优经验后给出的方案,没有特殊需求,初始值我会选大家都在用的伪标准。

——假行僧

在4核8线程的处理器使用Runtime.availableProcessors()结果是8,超线程技术属于硬件层面上的并发,从cpu硬件来看是一个物理核心有两个逻辑核心,但因为缓存、执行资源等存在共享和竞争,所以两个核心并不能并行工作。超线程技术统计性能提升大概是30%左右,并不是100%。另外,不管设置成4还是8,现代操作系统层面的调度应该是按逻辑核心数,也就是8来调度的(除非禁用超线程技术)。所以我觉得这种情况下,严格来说,4和8都不一定是合适的,具体情况还是要根据应用性能和资源的使用情况进行调整。

——榣山樵客™

工作中都是按照逻辑核数来的,理论值和经验值只是提供个指导,实际上还是要靠压测。

——作者


文章作者: Wendell
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Wendell !
评论
  目录