该知识点是我在重温线程池部分的内容时偶然接触到的,原文是美团技术团队发表的这篇文章。
线程池工作流程:
首先来重温一下线程池的工作流程,如上图所示。该图同样出自美团的那篇文章。
现存问题:
线程池使用面临的核心的问题在于:线程池的参数并不好配置。
一方面线程池的运行机制不是很好理解,配置合理需要强依赖开发人员的个人经验和知识;另一方面,线程池执行的情况和任务类型相关性较大,IO密集型和CPU密集型的任务运行起来的情况差异非常大,这导致业界并没有一些成熟的经验策略帮助开发人员参考。
可能的解决方案:
1.不用线程池
业务中使用线程池是为了提高并发性,是否有其它方案能够代替呢?
以上三种方法在理论上都能够代替线程池,但目前我们考虑的是如何更简单更高效更安全地获得并发性能,并且上述方案尚未成熟,不足够易用。
2.追求合理的参数设置
能否通过公式计算得到线程池中参数的最佳配置情况。业界中的部分参数配置方案如下图。
业界虽然给出了相应的计算公式,但都是在理论情况下。比如第三个,美团的流量分布情况一定不是均衡的,中午12点与凌晨3点的流量不可能呈平均分布状态。
并发任务的执行与任务类型有关,CPU密集型任务和IO密集型任务运行起来差异巨大,并且占比难以预测,很难总结出一个通用公式去直接计算结果。
3.线程池参数动态化
如果无法找到合理的参数配置,那能否换一个角度考虑,降低修改参数配置的成本,这样即使即使后续发生故障也能够快速调整并缩短故障时间。基于此,我们进一步考虑能否将线程池的参数从代码迁移至分布式配置中心上,实现参数的动态配置与调整。
动态更新代码Demo:
在示例中,设置的线程池参数分别是:核心线程数为2,最大线程数为5,任务队列为10,空闲线程存活时间为60秒。
然后基于它15个耗时10秒的任务。该线程池最多5个线程并且都在工作,剩余10个任务存在任务队列中。也就是5个工作线程耗时10秒执行任务,执行完毕后取出任务队列中的前5个继续执行10秒,完成后再取出剩下的5个,执行10秒。共执行30秒。
如果在提交任务时,将核心线程数和最大线程数都修改为10,那么此时会有10个任务直接被线程执行,5个任务存到任务队列中。10个线程执行10个任务10秒,然后剩下的5个取出继续执行10秒,因此只需要20秒即可完成。
实现原理:
setCorePoolSize方法:
在运行期线程池使用方调用此方法设置corePoolSize之后,线程池会直接覆盖原来的corePoolSize值,并且基于当前值和原始值的比较结果采取不同的处理策略。
对于当前值小于当前工作线程数的情况,说明有多余的worker线程,此时会向当前idle的worker线程发起中断请求以实现回收,多余的worker在下次idel的时候也会被回收;
对于当前值大于原始值且当前队列中有待执行任务,则线程池会创建新的worker线程来执行队列任务。
比如:上述Demo中,原始值是2,当前值是10,并且有待执行的任务,那么会创建新线程去处理任务。如果当前工作线程数,或者说是活跃线程数为10,而新设置的核心线程数为5,那么多出的5个线程将会占用额外的资源,可以进行回收。
关于这一点当前值小于当前工作线程数的情况,ChatGPT给出的回答是这样的:
具体的工作流程:
setMaximumPoolSize方法:
-
首先是参数合法性校验。
-
然后用传递进来的值,覆盖原来的值。
-
判断工作线程是否是大于最大线程数,如果大于,则对空闲线程发起中断请求。
由以上两个方法可知,核心线程数与最大线程数均可以动态调整。
注意:在修改核心线程参数时,可以将最大线程数与核心线程数同时修改为相同值,避免在实际程序运行中活跃线程数不变的情况。
提问:如果把核心线程数调的过大,那在业务低峰期的时候是否需要人工调低这个值呢?
回答:并不需要。corePoolSize中有一个参数叫做allowThreadTimeOut,它默认为flase,如果设置为true,就意味着当核心线程空闲时,keepAliveTime也会回收该核心线程,相当于线程池动态修改了。
队列长度如何动态指定?
文章中没有介绍具体的关于队列长度的set方法,但其实现思路是:
而为什么没有提供队列长度的set方法呢?是因为线程池参数中队列的capacity被final修饰了,无法更改。因此额外实现了一个自定义的任务队列。
把 LinkedBlockingQueue 代码粘贴一份出来,修改个名字,然后把 Capacity 参数的 final 修饰符去掉,并提供其对应的 get/set 方法。 然后在程序里面把原来的队列换掉即可。
几个知识点扩充:
1.线程池被创建后里面有线程吗?如果没有的话,有什么方法对线程池进行预热吗?
线程池被创建后如果没有任务过来,里面是不会有线程的。如果需要预热的话可以调用下面的两个方法:
启动全部线程:
启动一个线程:
2.核心线程会被回收吗?应该如何设置?
核心线程默认不会被回收,如果需要回收,需要调用下面的方法(allowCoreThreadTimeOut):