java面试

深入剖析Java线程池原理

2019-05-22  本文已影响187人  AKyS佐毅

1、什么是线程池:

java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池

2、为什么用线程池

多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。
假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。
如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。
一个线程池包括以下四个基本组成部分:

线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了。

线程池不仅调整T1,T3产生的时间段,而且它还显著减少了创建线程的数目,看一个例子:

总的来说:

线程池的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果任务数量超过了最大线程数量,需要进入队列排队等候,等其他线程执行完毕,再从队列中取出任务来执行

主要特点:

带来的好处:

3、深入理解线程池

image.png image.png image.png image.png

4、四种拒绝策略

image.png

5、线程池实际生产中的运用

阿里巴巴手册

image.png image.png

6、那究竟多少个线程合适呢?

大家都写过多线程应用,也都用过线程池,那么线程池究竟设置多少个合适呢?

想必这个问题困恼着我们很多人,我们中的大部分都是拍着脑袋决定的。针对 CPU 密集型的程序比较简单我们就不讨论了,但是对于 IO 密集型的程序究竟设置多少合适呢?

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

公式中提到的阻塞系数决定于你的程序的 IO 延迟程度,当然这也是需要通过经验评估出来的,下面的图呢就是一个具体例子测试情况,可以看出来线程池的大小对响应时间的比例,并不是线程越多越快,针对每一个程序都会有一个合适的点。当然这个公式给我们提供的是一个解决思路,并不是实际的数量,毕竟我们的程序的性能也是参差不齐的。所以我们需要参考上面的公式,结合自己程序的“阻塞”时间合理的评估一个数量。

image

为便于你理解,这里我举个简单的例子说明一下:计算 1+2+... ... +100 亿的值,如果在 4 核的 CPU 上利用 4 个线程执行,线程 A 计算 [1,25 亿),线程 B 计算 [25 亿,50 亿),线程 C 计算 [50,75 亿),线程 D 计算 [75 亿,100 亿],之后汇总,那么理论上应该比一个线程计算 [1, 100 亿] 快将近 4 倍,响应时间能够降到 25%。一个线程,对于 4 核的 CPU,CPU 的利用率只 有 25%,而 4 个线程,则能够将 CPU 的利用率提高到 100%。

image.png

创建多少线程合适?

创建多少线程合适,要看多线程具体的应用场景。我们的程序一般都是 CPU 计算和 I/O 操作交 叉执行的,由于 I/O 设备的速度相对于 CPU 来说都很慢,所以大部分情况下,I/O 操作执行的 时间相对于 CPU 计算来说都非常长,这种场景我们一般都称为 I/O 密集型计算;和 I/O 密集型 计算相对的就是 CPU 密集型计算了,CPU 密集型计算大部分场景下都是纯 CPU 计算。I/O 密集 型程序和 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 个线程,如下图所示:CPU 在 A、B、C 三个线程之间切换,对于线程 A,当 CPU 从 B、C 切换回来时,线程 A 正好执行完 I/O 操作。这样 CPU 和 I/O 设备的利用率都达到了 100%。

image

通过上面这个例子,我们会发现,对于 I/O 密集型计算场景,最佳的线程数是与程序中 CPU 计 算和 I/O 操作的耗时比相关的,我们可以总结出这样一个公式:

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

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

不过上面这个公式是针对单核 CPU 的,至于多核 CPU,也很简单,只需要等比扩大就可以了, 计算公式如下:

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

总结

很多人都知道线程数不是越多越好,但是设置多少是合适的,却又拿不定主意。其实只要把握住 一条原则就可以了,这条原则就是将硬件的性能发挥到极致。上面我们针对 CPU 密集型和 I/O 密集型计算场景都给出了理论上的最佳公式,这些公式背后的目标其实就是将硬件的性能发挥到 极致。

对于 I/O 密集型计算场景,I/O 耗时和 CPU 耗时的比值是一个关键参数,不幸的是这个参数是 未知的,而且是动态变化的,所以工程上,我们要估算这个参数,然后做各种不同场景下的压测 来验证我们的估计。不过工程上,原则还是将硬件的性能发挥到极致,所以压测时,我们需要重 点关注 CPU、I/O 设备的利用率和性能指标(响应时间、吞吐量)之间的关系。

具体如何查看IO情况,请参考下边的文章

inux wa%过高,iostat查看io状况

7、推荐阅读

线程池没你想的那么简单
如何优雅的使用和理解线程池

详情介绍:

更新主题详情

420天以来,Java架构更新了 888个主题,已经有156+位同学加入。微信扫码关注java架构,获取Java面试题和架构师相关题目和视频。上述相关面试题答案,尽在Java架构中。

上一篇 下一篇

猜你喜欢

热点阅读