public void execute(Runnable command) {
…
int c = ctl.get();
if (workerCountOf© < corePoolSize) { // workerCountOf() 方法是获取线程池内线程数量
if (addWorker(command, true))
return;
c = ctl.get();
}
…
}
而文档里的 running 状态也指的是线程已经被创建,我们也知道线程被创建后,会在一个 while 循环里尝试从 BlockingQueue 里获取并执行任务,说它正在 running 也不为过。
基于此,我们对一些高并发服务进行的预热,其实并不是期望 JVM 能对热点代码做 JIT 等优化,对线程池、连接池和本地缓存的预热才是重点。
BlockingQueue
BlockingQueue 是线程池内的另一个重要组件,首先它是线程池”生产者-消费者”模型的中间媒介,另外它也可以为大量突发的流量做缓冲,但理解和配置它也经常会出错。
运行模型
最常见的错误是不理解线程池的运行模型。首先要明确的一点是线程池并没有准确的调度功能,即它无法感知有哪些线程是处于空闲状态的,并把提交的任务派发给空闲线程。线程池采用的是”生产者-消费者”模式,除了触发线程创建的任务(线程的 firstTask)不会入 BlockingQueue 外,其他任务都要进入到 BlockingQueue,等待线程池内的线程消费,而任务会被哪个线程消费到完全取决于操作系统的调度。
对应的生产者源码如下:
public void execute(Runnable command) {
…
if (isRunning© && workQueue.offer(command)) { isRunning() 是判断线程池处理戚状态
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
…
}
对应的消费者源码如下:
private Runnable getTask() {
for (;😉 {
…
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
…
}
}
BlockingQueue 的缓冲作用
基于”生产者-消费者”模型,我们可能会认为如果配置了足够的消费者,线程池就不会有任何问题。其实不然,我们还必须考虑并发量这一因素。
设想以下情况:有 1000 个任务要同时提交到线程池内并发执行,在线程池被初始化完成的情况下,它们都要被放到 BlockingQueue 内等待被消费,在极限情况下,消费线程一个任务也没有执行完成,那么这 1000 个请求需要同时存在于 BlockingQueue 内,如果配置的 BlockingQueue Size 小于 1000,多余的请求就会被拒绝。
那么这种极限情况发生的概率有多大呢?答案是非常大,因为操作系统对 I/O 线程的调度优先级是非常高的,一般我们的任务都是由 I/O 的准备或完成(如 tomcat 受理了 http 请求)开始的,所以很有可能被调度到的都是 tomcat 线程,它们在一直往线程池内提交请求,而消费者线程却调度不到,导致请求堆积。
我负责的服务就发生过这种请求被异常拒绝的情况,压测时 QPS 2000,平均响应时间为 20ms,正常情况下,40 个线程就可以平衡生产速度,不会堆积。但在 BlockingQueue Size 为 50 时,即使线程池 coreSize 为 1000,还会出现请求被线程池拒绝的情况。
这种情况下,BlockingQueue 的重要的意义就是它是一个能长时间存储任务的容器,能以很小的代价为线程池提供缓冲。根据上文可知,线程池能支持BlockingQueue Size
个任务同时提交,我们把最大同时提交的任务个数,称为并发量,配置线程池时,了解并发量异常重要。
并发量的计算
我们常用 QPS 来衡量服务压力,所以配置线程池参数时也经常参考这个值,但有时候 QPS 和并发量有时候相关性并没有那么高,QPS 还要搭配任务执行时间
来推算
峰值并发量。
比如请求间隔严格相同的接口,平均 QPS 为 1000,它的并发量峰值是多少呢?我们并没有办法估算,因为如果任务执行时间为 1ms,那么它的并发量只有 1;而如果任务执行时间为 1s,那么并发量峰值为 1000。
可是知道了任务执行时间,就能算出并发量了吗?也不能,因为如果请求的间隔不同,可能 1min 内的请求都在一秒内发过来,那这个并发量还要乘以 60,所以上面才说知道了 QPS 和任务执行时间,并发量也只能靠推算。
计算并发量,我一般的经验值是 QPS*平均响应时间
,再留上一倍的冗余,但如果业务重要的话,BlockingQueue Size 设置大一些也无妨(1000 或以上),毕竟每个任务占用的内存量很有限。
考虑运行时
GC
除了上面提到的各种情况下,GC 也是一个很重要的影响因素。
我们都知道 GC 是 Stop the World 的,但这里的 World 指的是 JVM,而一个请求 I/O 的准备和完成是操作系统在进行的,JVM 停止了,但操作系统还是会正常受理请求,在 JVM 恢复后执行,所以 GC 是会堆积请求的。
上文中提到的并发量计算一定要考虑到 GC 时间内堆积的请求同时被受理的情况,堆积的请求数可以通过 QPS*GC时间
来简单得出,还有一定要记得留出冗余。
业务峰值
除此之外,配置线程池参数时,一定要考虑业务场景。
假如接口的流量大部分来自于一个定时程序,那么平均 QPS 就没有了任何意义,线程池设计时就要考虑给 BlockingQueue 的 Size 设置一个大一些的值;而如果流量非常不平均,一天内只有某一小段时间才有高流量的话,而且线程资源紧张的情况下,就要考虑给线程池的 maxSize 留下较大的冗余;在流量尖刺明显而响应时间不那么敏感时,也可以设置较大的 BlockingQueue,允许任务进行一定程度的堆积。
当然除了经验和计算外,对服务做定时的压测无疑更能帮助掌握服务真实的情况。
小结
–
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://img-blog.csdnimg.cn/img_convert/46cd716874db5a9af714a7278ed472d8.jpeg)
难道这样就够了吗?不,远远不够!
提前多熟悉阿里往年的面试题肯定是对面试有很大的帮助的,但是作为技术性职业,手里有实打实的技术才是你面对面试官最有用的利器,这是从内在散发出来的自信。
备战阿里时我花的最多的时间就是在学习技术上,占了我所有学习计划中的百分之70,这是一些我学习期间觉得还是很不错的一些学习笔记
我为什么要写这篇文章呢,其实我觉得学习是不能停下脚步的,在网络上和大家一起分享,一起讨论,不单单可以遇到更多一样的人,还可以扩大自己的眼界,学习到更多的技术,我还会在csdn、博客、掘金等网站上分享技术,这也是一种学习的方法。
今天就分享到这里了,谢谢大家的关注,以后会分享更多的干货给大家!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
存中…(img-QnUH1Szj-1713395990066)]
[外链图片转存中…(img-NjE3WGT5-1713395990066)]
[外链图片转存中…(img-ys2RWlDd-1713395990066)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!