如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。

荒腔走板

你们好,我是 why,一个四川好男人。html

今天原本应该是武汉马拉松鸣枪起跑的日子,因此先荒腔走板说几句马拉松吧。java

上面的图是我跑 2019 年成都马拉松的时候拍的,是一对双胞胎陪着 80 岁的父亲跑全程马拉松。面试

图片中的老人叫罗广德,在他 75 岁以前的人生和其余的老人并没有不一样。编程

可是通过他儿子的影响,在 75 岁的时候开始接触跑步的。一直就没有停下脚步,世界六大马拉松赛(纽约、伦敦、柏林、芝加哥、东京、波士顿)他已经完成了五个。服务器

原本打算今年 4 月份站上波士顿马拉松的赛道上,完成最后的挑战。微信

完成以后,他就是世界华人这个年龄段里第一个完成世界六大马拉松赛的大满贯跑者。并发

可是因为疫情的缘由,波士顿马拉松延期举行了。可是没有关系,我相信老爷子的执着,我也相信他会是第一人。less

他说:“人生没有太晚的开始,关键是要行动起来。如今的年轻朋友不少都缺少锻炼,做息时间很差,我但愿年轻人都行动起来,我 80 岁都能跑步,难道大家不能跑吗?”运维

我以前说过,在赛道上你能看到不少有趣的、感动的画面。我喜欢跑马拉松,由于跑完以后老是能带给我爆棚的正能量。分布式

人生须要一场马拉松,你能够迟到,可是你不能缺席。

好了,说回文章。

经典面试题

此次的文章仍是绕回了我写的第三篇原创文章《有的线程它死了,因而它变成一道面试题》中留下的几个问题:

哎,兜兜转转,走走停停。天道好轮回,苍天饶过谁?

在这篇文章中我主要回答上面抛出的这个问题:你这几个参数的值怎么来的呀?

要回答这个问题,咱们得先说说这几个参数是什么,请看截图:

其实,官方的注释写的都很是明白了。你看文章的时必定要结合英文,由于英文是 Doug Lea(做者)他本身写的,表达的是做者本身的准确的想法。

不要瞎猜好吗?

1.corePoolSize:the number of threads to keep in the pool, even if they are idle, unless {@code allowCoreThreadTimeOut} is set

(核心线程数大小:无论它们建立之后是否是空闲的。线程池须要保持 corePoolSize 数量的线程,除非设置了 allowCoreThreadTimeOut。)

2.maximumPoolSize:the maximum number of threads to allow in the pool。

(最大线程数:线程池中最多容许建立 maximumPoolSize 个线程。)

3.keepAliveTime:when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating。

(存活时间:若是通过 keepAliveTime 时间后,超过核心线程数的线程尚未接受到新的任务,那就回收。)

4.unit:the time unit for the {@code keepAliveTime} argument

(keepAliveTime 的时间单位。)

5.workQueue:the queue to use for holding tasks before they are executed. This queue will hold only the {@code Runnable} tasks submitted by the {@code execute} method。

(存放待执行任务的队列:当提交的任务数超过核心线程数大小后,再提交的任务就存放在这里。它仅仅用来存放被 execute 方法提交的 Runnable 任务。因此这里就不要翻译为工做队列了,好吗?不要本身给本身挖坑。)

6.threadFactory:the factory to use when the executor creates a new thread。

(线程工程:用来建立线程工厂。好比这里面能够自定义线程名称,当进行虚拟机栈分析时,看着名字就知道这个线程是哪里来的,不会懵逼。)

7.handler:the handler to use when execution is blocked because the thread bounds and queue capacities are reached。

(拒绝策略:当队列里面放满了任务、最大线程数的线程都在工做时,这时继续提交的任务线程池就处理不了,应该执行怎么样的拒绝策略。)

7 个参数介绍完了,我但愿当面试官问你自定义线程池能够指定哪些参数的时候,你能回答的上来。

固然,不能死记硬背,这样回答起来磕磕绊绊的,像是在背书。也最好别给我回答什么:我给你举个例子吧,就是一开始有多少多少工人....

不必,真的,直接回答每一个参数的名称和含义就好了,牛逼的话你就给我说英文也行,我也能听懂。

这玩意你们都懂,又不抽象,你举那例子干啥?拖延时间吗?

面试要求的是尽可能精简、准确的回答问题,不要让面试官去你冗长的回答中提炼关键字。

一是面试官面试体验很差。面试完了后,经常是面试者在强调本身的面试体验。朋友,你多虑了,你面试体验很差,回去一顿吐槽,叫你进入下一轮面试的时候,大部分人还不是腆着个脸就来了。面试官的体验很差,那你是真的没有下一轮了。

二是面试官面试都是有必定的时间限制的,有限的面试时间内,前面太啰嗦了,能问你的问题就少了。问的问题少了,面试官写评分表的时候一想,我靠,还有好多问题没问呢,也不知道这小子能不能回答上来,算了,就不进入下一轮了吧。

好了好了,一不下心又暴露了几个面试小技巧,扯远了,说回来。

上面的 7 个参数中,咱们主要须要关心的参数是:corePoolSize、maximumPoolSize、workQueue(队列长度)

因此,文本主要讨论这个问题:

当咱们自定义线程池的时候 corePoolSize、maximumPoolSize、workQueue(队列长度)该如何设置?

你觉得我要给你讲分 IO 密集型任务或者分 CPU 密集型任务?

不会的,说好的是让面试官眼前一亮、虎躯一震、直呼牛皮的答案。不骗你。

美团骚操做

怎么虎躯一震的呢?

由于我看到了美团技术团队发表的一篇文章:《Java线程池实现原理及其在美团业务中的实践》

第一次看到这篇文章的时候我真是眼前一亮,看到美团的这骚操做,我真是直呼牛皮。

(哎,仍是本身见的太少了。)

这篇文章写的很好,很全面,好比我以前说的线程执行流程,它配了一张图,一图胜千言:

阻塞队列成员表,尽收眼底:

前面都是些基础知识,文中的后半部分才抛出了一个实际问题:

线程池使用面临的核心的问题在于:线程池的参数并很差配置。


一方面线程池的运行机制不是很好理解,配置合理须要强依赖开发人员的我的经验和知识;


另外一方面,线程池执行的状况和任务类型相关性较大,IO密集型和CPU密集型的任务运行起来的状况差别很是大。


这致使业界并无一些成熟的经验策略帮助开发人员参考。

美团给出的对应的解决方案是什么呢?

线程池参数动态化。

尽管通过谨慎的评估,仍然不可以保证一次计算出来合适的参数,那么咱们是否能够将修改线程池参数的成本降下来,这样至少能够发生故障的时候能够快速调整从而缩短故障恢复的时间呢?


基于这个思考,咱们是否能够将线程池的参数从代码中迁移到分布式配置中心上,实现线程池参数可动态配置和即时生效,线程池参数动态化先后的参数修改流程对好比下:

说实话看到这个图的时候我想起以前也有这样的想法的。

由于有一次我这边有个项目里面的定时任务用到了线程池,可是核心线程数和队列长度都设置的比较大,某一次任务触发后查出了大批数据,经过线程池提交任务,每一个任务里面都会调用下游服务,致使下游服务长时间的压力过大,也没有作限流,因此影响了其对外提供的其余功能。

因而我叫运维帮我在 Apollo(配置中心)调小了核心线程数,而且重启了服务。

那一次我就在想,咱们使用的是 Apollo 自然支持动态更新,那我能不能动态的修改线程池呢?

由于那个时候不知道一个构建好了的线程池,它的核心线程数和最大线程数是能够动态修改的。

因此最开始的想法是监听到参数变化后,直接弄一个新的线程池把原来的给替换掉。

但这样的问题是,偷天换日以后,原来的线程池里面的任务我怎么处理呢?

我不能等原来的线程池里面的任务执行完成后再换,由于这个时候任务必定是源源不断的过来的。

因而就卡在了这个地方。

说来惭愧,这块源码我看过几回,但仍是差点火候,学艺不精,怨不得别人。

先劝退一波

为了避免浪费你的时间,先检测一下你是否有阅读本文的基础知识储备:

首先,咱们先自定义一个线程池:

拿着这个线程池,当这个线程池在正常工做的前提下,我先问你两个问题:

1.若是这个线程池接受到了 30 个比较耗时的任务,这个时候线程池的状态(或者说数据)是怎样的?

2.在前面 30 个比较耗时的任务还没执行完成的状况下,再来多少个任务会触发拒绝策略?

其实这就是在问你线程池的执行流程了,简单的说一下就是:

1.当接收到了 30 个比较耗时的任务时,10 个核心线程数都在工做,剩下的 20 个去队列里面排队。这个时候和最大线程数是没有关系的,因此和线程存活时间也就没有关系。

2.其实你知道这个线程池最多能接受多少任务,你就知道这个题的答案是什么了,上面的线程池中最多接受 1000(队列长度) + 30(最大线程数) = 1030 个任务。因此当已经接收了30个任务的状况下,若是再来 1000 个比较耗时的任务,这个时候队列也满了,最大线程数的线程也都在工做,这个时候线程池满载了。所以,在前面 30 个比较耗时的任务还没执行完成的状况下,再来 1001 个任务,第 1001 个任务就会触发线程池的拒绝策略了。

这两个问题你得会,若是答不上来你也别往下看了,大几率看的一脸懵逼。

我建议你先给本文点个赞,接着去网上搜一下线程池执行流程的文章(其实美团的那篇文章也写了执行流程),写个 Demo 跑一下,摸清楚了,再来看这篇文章。

巨人肩膀

对于线程池参数到底如何设置的问题美团的那篇文章提供了一个很好的思路和解决方案,展示的是一个大而全的东西。

可是,对于实施起来的细节就没有具体的展现了。

因此文本斗胆,站在巨人的肩膀上对细节处进行一些补充说明。

1.现有的解决方案的痛点。

2.动态更新的工做原理是什么?

3.动态设置的注意点有哪些?

4.如何动态指定队列长度?

5.这个过程当中涉及到的面试题有哪些?

下面从这五点进行展开说明。

现有的解决方案的痛点。

如今市面上大多数的答案都是先区分线程池中的任务是 IO 密集型仍是 CPU 密集型。

若是是 CPU 密集型的,能够把核心线程数设置为核心数+1。

为何要加一呢?

《Java并发编程实战》一书中给出的缘由是:即便当计算(CPU)密集型的线程偶尔因为页缺失故障或者其余缘由而暂停时,这个“额外”的线程也能确保 CPU 的时钟周期不会被浪费。

看不懂是否是?不要紧我也看不懂。反正把它理解为一个备份的线程就好了。

这个地方还有个须要注意的小点就是,若是你的服务器上部署的不止一个应用,你就得考虑其余的应用的线程池配置状况。

通过精密的计算,你咔一下设置为核心数,结果项目部署上去了,发现还有其余的应用在和你抢 CPU,你想一想难不难受。

若是是包含 IO 操做的任务呢?这个才是咱们关心的东西。

《Java并发编程实战》一书中给出的计算方式是这样的:

理想很丰满,现实很骨感。

我以前有个系统就是按照这个公式算出来的参数去配置的。

结果效果并很差,甚至让下游系统直呼受不了。

这个东西怎么说呢,仍是得记住,面试的时候有用。真实场景中只能获得一个参考值,基于这个参考值,再去进行调整。

咱们再看一下美团的那篇文章调研的现有解决方案列表:

第一个就是咱们上面说的,和实际业务场景有所偏离。

第二个设置为 2*CPU 核心数,有点像是把任务都当作 IO 密集型去处理了。并且一个项目里面通常来讲不止一个自定义线程池吧?好比有专门处理数据上送的线程池,有专门处理查询请求的线程池,这样去作一个简单的线程隔离。可是若是都用这样的参数配置的话,显然是不合理的。

第三个不说了,理想状态。流量是不可能这么均衡的,就拿美团来讲,下午3,4点的流量,能和 12 点左右午餐时的流量比吗?

基于上面的这些解决方案的痛点,美团给出了动态化配置的解决方案。

动态更新的工做原理是什么?

先来一个动态更新的代码示例:

上面的程序就是自定义了一个核心线程数为 2,最大线程数为 5,队列长度为 10 的线程池。

而后给它塞 15 个耗时 10 秒的任务,直接让它 5 个最大线程都在工做,队列长度 10 个都塞满。

当前的状况下,队列里面的 10 个,前 5 个在 10 秒后会被执行,后 5 个在 20 秒后会被执行。

再加上最大线程数正在执行的 5 个,15 个任务所有执行彻底须要 3 个 10 秒即 30 秒的时间。

这个时候,若是咱们把核心线程数和最大线程数都修改成 10。

那么 10 个任务会直接被 10 个最大线程数接管,10 秒就会被处理完成。

剩下的 5 个任务会在 10 秒后被执行完成。

因此,15 个任务执行完成须要 2 个 10 秒即 20 秒的时间处理完成了。

看一下上面程序的打印日志:

效果实现了,我先看一下原理是什么。

先看 setCorePoolSize 方法:

这个方法在美团的文章中也说明了:

在运行期线程池使用方调用此方法设置corePoolSize以后,线程池会直接覆盖原来的corePoolSize值,而且基于当前值和原始值的比较结果采起不一样的处理策略。

对于当前值小于当前工做线程数的状况,说明有多余的worker线程,此时会向当前idle的worker线程发起中断请求以实现回收,多余的worker在下次idel的时候也会被回收;

对于当前值大于原始值且当前队列中有待执行任务,则线程池会建立新的worker线程来执行队列任务,setCorePoolSize具体流程以下:

看了美团的那篇文章后,我又去看了 Spring 的 ThreadPoolTaskExecutor类 (就是对JDK ThreadPoolExecutor 的一层包装,能够理解为装饰者模式)的 setCorePoolSize 方法: 注释上写的清清楚楚,能够在线程池运行时修改该参数。

并且,你再品一品 JDK 的源码,其实源码也体现出了有修改的含义的,两个值去作差值,只是第一次设置的时候原来的值为 0 而已。

哎,当时没有细细研究,恨本身看源码的时候不仔细。

接着看 setMaximumPoolSize 源码:

这个地方就很简单了,逻辑不太复杂。

1.首先是参数合法性校验。

2.而后用传递进来的值,覆盖原来的值。

3.判断工做线程是不是大于最大线程数,若是大于,则对空闲线程发起中断请求。

通过前面两个方法的分析,咱们知道了最大线程数和核心线程数能够动态调整。

动态设置的注意点有哪些?

调整的时候可能会出现核心线程数调整以后无效的状况,好比下面这种:

改变以前的核心线程数是 2,最大线程数为 5,咱们动态修改核心线程数为 10。

可是从日志仍是能够看出,修改以后核心线程数确实变成了 10,但活跃线程数仍是为 5。

并且我调用了 prestartCoreThread 方法,该方法见名知意,你也知道是启动全部的核心线程数,全部不存在线程没有建立的问题。

这是为何呢?

源码之下无秘密,我带你去看一眼:

java.util.concurrent.ThreadPoolExecutor#getTask

在这个方法中咱们能够看到,若是工做线程数大于最大线程数,则对工做线程数量进行减一操做,而后返回 null。

因此,这个地方的实际流程应该是: 建立新的工做线程 worker,而后工做线程数进行加一操做。 运行建立的工做线程 worker,开始获取任务 task。 工做线程数量大于最大线程数,对工做线程数进行减一操做。 返回 null,即没有获取到 task。 清理该任务,流程结束。

这样一加一减,因此真正在执行任务的工做线程数的数量一直没有发生变化,也就是最大线程数。

怎么解决这个问题呢?

答案已经呼之欲出啦。

设置核心线程数的时候,同时设置最大线程数便可。其实能够把两者设置为相同的值:

这样,活动线程数就能正常提升了。

有的小伙伴就会问了:若是调整以后把活动线程数设置的值太大了,岂不是业务低峰期咱们还须要人工把值调的小一点?

不存在的,还记得前面介绍 corePoolSize 参数的含义时的注解吗:

当 allowCoreThreadTimeOut 参数设置为 true 的时候,核心线程在空闲了 keepAliveTime 的时间后也会被回收的,至关于线程池自动给你动态修改了。

如何动态指定队列长度?

前面介绍了最大线程数和核心线程数的动态设置,可是你发现了吗,并无设置队列长度的 set 方法啊?

有的小机灵鬼说先获取 Queue 对象出来再看一下呢?

仍是没有,这可咋整呢?

首先咱们看一下为何没有提供队列长度的 set 方法呢:

由于队列的 capacity 是被 final 修饰了呀。

可是美团的那篇文章明明说了,他们也支持队列的动态调整呀:

但是没有详细说明,可是别着急,接着看后面的内容能够发现他们有一个名字为 ResizableCapacityLinkedBlockIngQueue 的队列:

很明显,这是一个自定义队列了。

咱们也能够按照这个思路自定义一个队列,让其能够对 Capacity 参数进行修改便可。

操做起来也很是方便,把 LinkedBlockingQueue 粘贴一份出来,修改个名字,而后把 Capacity 参数的 final 修饰符去掉,并提供其对应的 get/set 方法。

而后在程序里面把原来的队列换掉:

运行起来看看效果:

能够看到,队列大小确实从 10 变成了 100,队列使用度从 100% 降到了 9%。

我后来去看了美团的那篇文章下面的评论,有个评论是这样的:

果真不出我所料。

这个过程当中涉及到的面试题有哪些?

问题一:线程池被建立后里面有线程吗?若是没有的话,你知道有什么方法对线程池进行预热吗?

线程池被建立后若是没有任务过来,里面是不会有线程的。若是须要预热的话能够调用下面的两个方法:

所有启动:

仅启动一个:

问题二:核心线程数会被回收吗?须要什么设置?

核心线程数默认是不会被回收的,若是须要回收核心线程数,须要调用下面的方法:

allowCoreThreadTimeOut 该值默认为 false。

最后说一句(求关注)

点个赞吧,周更很累的,不要白嫖我,须要一点正反馈。

才疏学浅,不免会有纰漏,若是你发现了错误的地方,因为本号没有留言功能,还请你加我微信给我指出来,我对其加以修改。(我每篇技术文章都有这句话,我是认真的说的。)

感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。

我是why技术,一个不是大佬,可是喜欢分享,又暖又有料的四川好男人。

欢迎关注公众号【why技术】,坚持输出原创。分享技术、品味生活,愿你我共同进步。

相关文章
相关标签/搜索