前言:曾经自诩对线程池了如指掌,不料看了美团的一篇技术文章后才知道原来线程池的参数还可以动态调节。
学艺不精,一边留下了没有技术的泪水,一边站在美团这个巨人的肩上写下此文,补充并记录了自己的一点看法。
分享给大家,希望能对你有所帮助。
荒腔走板
大家好,我是 why,一个四川好男人。
今天本来应该是武汉马拉松鸣枪起跑的日子,所以先荒腔走板说几句马拉松吧。
上面的图是我跑 2019 年成都马拉松的时候拍的,是一对双胞胎陪着 80 岁的父亲跑全程马拉松。
图片中的老人叫罗广德,在他 75 岁之前的人生和其他的老人并无不同。
但是经过他儿子的影响,在 75 岁的时候开始接触跑步的。一直就没有停下脚步,世界六大马拉松赛(纽约、伦敦、柏林、芝加哥、东京、波士顿)他已经完成了五个。
本来打算今年 4 月份站上波士顿马拉松的赛道上,完成最后的挑战。
完成之后,他就是世界华人这个年龄段里第一个完成世界六大马拉松赛的大满贯跑者。
但是由于疫情的原因,波士顿马拉松延期举行了。但是没有关系,我相信老爷子的执着,我也相信他会是第一人。
他说:“人生没有太晚的开始,关键是要行动起来。现在的年轻朋友很多都缺乏锻炼,作息时间不好,我希望年轻人都行动起来,我 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 天然支持动态更新,那我能不能动态的修改线程池呢?
因为那个时候不知道一个构建好了的线程池,它的核心线程数和最大线程数是可以动态修改的。
所以最开始的想法是监听到参数变化后,直接弄一个新的线程池把原来的给替换掉。
但这样的问题是,偷天换日之后,原来的线程池里面的任务我怎么处理呢?
我不能等原来的线程池里面的任务执行完成后再换,因为这个时候任务一定是源源不断的过来的。
于是就卡在了这个地方。
说来惭愧,这块源码我看过几次,但还是差点火候,学艺不精,怨不得别人。