首先需要分析2个问题:

  1. 为什么要使用多线程
  2. 多线程的应用场景

为什么要使用多线程

一个字,。为了提升性能。那么性能该如何用理性的数字而非感性的认知来量化呢?性能有两个核心指标:延迟吞吐量

延迟:发出请求到收到响应需要的时间,延迟越小,单个请求的执行速度越快,性能越好。

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

这两个指标内部有一定的联系:同等条件下,延迟越低则吞吐量越大,但是这两个指标一个属于时间维度一个属于空间维度,不能互相转换。

提升性能则是从降低延迟``,提高吞吐量着手,这也是我们使用多线程的主要目的。

多线程的应用场景

提升性能的两个方向:

  1. 优化算法
  2. 提高硬件利用率

操作系统已经姐姐了硬件利用率:操作系统已经解决了磁盘和网卡利用率问题,利用中断机制可以避免 CPU 轮询 I/O 状态,提升了 CPU 利用率。但是操作系统解决硬件利用率问题的对象往往是单一的硬件设备,而我们编写的 Java 程序往往需要 CPUI/O 设备互相配合工作。

所以我们需要解决的是 CPUI/O 设备综合利用率的问题,这个问题的解决方法就是使用 多线程

如下图所示,如果只有一个线程,则执行I/O 操作的时候 CPU 是闲置的,执行 CPU 操作的时候 I/O 设备是闲置的,所以 CPUI/O 设备的利用率是 50%

img

单线程执行示意图

如果有两个相乘,就可以在 CPU 计算时 I/O 设备同时读写数据,这样 CPU 和 I/O 设备的利用率就达到了 100%,时刻都有线程使用这两个硬件设备。

img

双线程执行示意图

而从单线程两个线程的直接性能表现就是 单位时间处理的请求数量翻倍吞吐量提高一倍,所以如果我们发现 CPUI/O 设备的利用率都很低,则可以通过增加线程数量提高吞吐量

以上针对的是单核时代,多线程的主要作用就是用来 平衡 CPUI/O 设备之间的速度差异。但是如果程序是 CPU 密集型而不存在 I/O 操作的话,多线程反而会使性能变差,原因是 CPU 本身就一直满载了,而线程之间切换是存在开销的,相当于增加了额外的开销。

但是在 多核时代,每个 CPU 核心上都可以运行一个线程,所以即便是 CPU 密集型的程序,使用多线程也可以提升性能:

img

多核执行多线程示意图

举个简单的例子:

比如要计算 1-100亿的数的和是多少,在4核环境下,可以分配给4个线程:线程1 计算 1-25亿 线程2计算 25 - 50亿,线程3 计算 50 -75亿,线程4计算 75 -100亿,这就比使用单个线程要快接近4倍。

创建多少线程合适

分为两个场景,CPU密集型和I/O密集型。

  • CPU密集型:多线程本质上是提高多核 CPU 的利用率,所以对于 4核 CPU 来说,每个核一个线程,理论上创建4个就可以了,再多的话反而可能增加线程切换的成本。但是在工程上线程的数量一般会设置为 CPU 核数+1 这样的话当线程偶尔因为内存页失效其他原因导致阻塞时,这个额外的线程可以补上,保证 CPU 利用率。

  • I/O 密集型:如果 CPU 计算 和 I/O 操作耗时 1:1 ,那么2个是最合适的。 如果 CPU 操作和 I/O 操作耗时 1:2 ,则 三个线程最合适,如下图所示:CPU 在 ABC 三个线程之间切换,对于线程 A 来说,当 CPU 从 BC 切换回来时,线程A 正好执行完 I/O 操作。 这样 CPUI/O 的利用率都达到了 100%

img

三线程执行示意图

对于 I/O 密集型场景,最佳线程数与程序中 CPU 计算和 I/O 操作的耗时比相关,可以总结出如下公式:
$$
单核最佳线程数 = 1+(\frac{I/O耗时}{CPU耗时})
$$

令:
$$
R = \frac{I/O耗时}{CPU耗时}
$$

配合上图可以这样理解:当线程A 执行 I/O 操作时, 另有 R 个线程正好执行完各自的 CPU 计算,这样 CPU 的利用率就达到了 100%

上面针对的是单核,如果是多核等比扩大
$$
多核最佳线程数 = CPU核心数量 * [1+(\frac{I/O耗时}{CPU耗时})]
$$

个人总结:

这章的收获主要是针对不同场景该怎样设置线程数量的一个思路,我认为学知识首先针对场景,也就是需求,然后才有不同的配置,而不是去学一个固定的配置方法。

我们设置线程数值需要把握最大化硬件性能就可以,而这个前提就是我们将场景分析的比较清晰,是 I/O 密集型 还是 CPU 密集型,所以在进行压力测试的时候需要重点关注 CPU、I/O 设备的利用率和性能指标

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

最是人间留不住,曾是惊鸿照影来。