先劝退一波
为了不浪费你的时间,先检测一下你是否有阅读本文的基础知识储备:
首先,我们先自定义一个线程池:
拿着这个线程池,当这个线程池在正常工作的前提下,我先问你两个问题:
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.判断工作线程是否是大于最大线程数,如果大于,则对空闲线程发起中断请求。
经过前面两个方法的分析,我们知道了最大线程数和核心线程数可以动态调整。