在Java领域,实现并发程序的主要手段就是多线程。根据使用多线程的目的和场景合理设置线程数。
使用多线程的目的
使用多线程,本质上就是提升线程性能。[安全性、活跃性以及性能问题]
度量性能的指标有很多,其中两个核心的就是延迟和吞吐量。延迟指的是发出请求到收到响应这个过程的时间;延迟越短,意味着程序执行的越快,性能也越好。吞吐量指的是在单位时间内能处理请求的数量;吞吐量越大,意味着程序能处理的请求越多,性能也越好。这两个指标内部有一定的联系(同等条件下,延迟越短,吞吐量越大),但是由于它们隶属不同的维度(一个是时间维度,一个是空间维度),并不能互相转换。
所谓提升性能,从度量的角度,主要是降低延迟,提高吞吐量。这是使用多线程的主要目的。
多线程的应用场景
降低延迟,提高吞吐量。对应的方法有两个方向:一个方向是优化算法,另一个方向是将硬件的性能发挥到极致。
前者属于算法范畴,后者则是和并发编程息息相关。计算机的硬件主要是两。类:一个是I/O,一个是CPU。所以,在并发编程领域,提升性能本质上就是提升硬件的利用率,具体说就是提升I/O的利用率和CPU的利用率。
假如操作系统已经解决了磁盘和网卡的利用率问题,利用中断机制还能避免CPU轮询I/O状态,也提升了CPU的利用率。但是操作系统解决硬件利用率问题的对象往往是单一硬件设备,而并发程序,往往需要CPU和I/O设备湘湖配合工作。最终需要解决CPU和I/O设备综合利用率的问题,解决方案就是多线程。
如果CPU和I/O设备的利用率都很低,可以尝试通过增加线程来提高吞吐量。
在单核时代,多线程主要就是用来平衡CPU和I/O设备的。如果程序只有CPU计算,而没有I/O操作的话,多线程不但不会提升性能,还会使心梗变得更差,原因是增加了线程切换的成本。但是在多核时代,这种纯计算型的程序也可以利用多线程来提升性能,因为利用多核可以降低响应时间(将计算任务分配给多个核心,分别计算后再统计)。
创建多少线程合适
创建多少线程核实,要看多线程具体的应用场景。程序一般都是CPU计算和I/O操作交叉执行的,由于I/O设备的速度相对于CPU来说都很慢,大部分情况下,I/O操作执行的时间相对于CPU计算来说都非常长,这种场景一般称为I/O密集型计算;相对的就是CPU密集型计算,CPU密集型计算大部分场景下都是纯CPU计算。
对于CPU密集型计算,多线程本质上是提升多核CPU的利用率,所以对于一个4核的CPU,每个核一个线程,理论上创建4个线程就可以了,再多创建线程也只是增加线程切换的成本。所以,对于CPU密集型的计算场景,理论上“线程的数量=CPU核数”就是最合适的。在工程上,线程的数量一般会设置为“CPU核数+1”,这样当线程因为偶尔的内存页失效或者其他原因导致阻塞时,这个额外的线程可以顶上,从而保证CPU的利用率。
对于I/O密集型的计算,如果CPU计算和I/O操作的耗时是1:1,那么2个线程是最合适的。如果CPU计算和I/O操作的耗时是1:2,那么3个线程是合适的。
![]() |
![]() |
![]() |
综上对于I/O密集型计算场景,最佳的线程数是与程序中CPU计算和I/O操作的耗时比相关的,公式:
最佳线程数 = 1 + (I/O耗时/CPU耗时)
令R=I/O耗时/CPU耗时:当线程A执行IO操作时,另外R个线程正好执行完各自的CPU计算。这样CPU的利用率就达到了100%。
针对多核CPU,等比扩大就可以:
最佳线程数 = CPU核数 * [1 + (I/O耗时/CPU耗时)]
经验值:最佳线程数 = 2 * CPU的核数 + 1
总结
线程数不是越多越好。设置线程数把握住一条原则-将硬件的性能发挥到极致。
对于I/O密集型计算场景,I/O耗时和CPU耗时的比值是一个关键参数,这个参数是未知的、动态变化的。工程上要估算这个参数,做各种不同场景下的压测来验证估计。(apm工具测试耗时,精确到方法)