以下文章来源于why技能,作者why技能
根据读者转述,面试官的原问题便是:一个 SpringBoot 项目能一起处理多少恳求?
不知道你听到这个问题之后的榜首反响是什么。
我大概知道他要问的是哪个方向,可是关于这种只要一句话的面试题,我的榜首反响是:会不会有坑?
所以并不会轻率答题,先诘问一些音讯,比方:这个项目具体是干什么的?项目大概进行了哪些参数装备?运用的 web 容器是什么?布置的服务器装备如何?有哪些接口?接口响应均匀时间大概是多少?
这样,在几个问题的拉扯之后,至少在面试题调查的方向方面能基本和面试官达成了一致。
比方前面的面试问题,通过几次拉扯之后,面试官或许会修正为:
一个 SpringBoot 项目,未进行任何特殊装备,悉数采用默许设置,这个项目同一时间,最多能一起处理多少恳求?
能处理多少呢?
我也不知道,可是当问题变成上面这样之后,我找到了探索答案的视点。
已然“未进行任何特殊装备”,那我自己搞个 Demo 出来,压一把不就完事了吗?
坐稳扶好,预备发车。
Demo
小手一抖,先搞个 Demo 出来。
这个 Demo 十分的简单,便是通过 idea 创立一个全新的 SpringBoot 项目就行。
我的 SpringBoot 版本运用的是 2.7.13。
整个项目只要这两个依赖:
整个项目也只要两个类,要得便是一个空空如也,一清二白。
项目中的 TestController,里边只要一个 getTest 办法,用来测验,办法里边承受到恳求之后直接 sleep 一小时。
目的便是直接把当时恳求线程占着,这样咱们才能知道项目中总共有多少个线程能够运用:
@Slf4j
@RestController
publicclassTestController{
@GetMapping("/getTest")
publicvoidgetTest(intnum)throwsException{
log.info("{}承受到恳求:num={}",Thread.currentThread().getName(),num);
TimeUnit.HOURS.sleep(1);
}
}
项目中的 application.properties 文件也是空的:
这样,一个“未进行任何特殊装备”的 SpringBoot 不就有了吗?
根据这个 Demo,前面的面试题就要变成了:我短时间内不断的调用这个 Demo 的 getTest 办法,最多能调用多少次?
问题是不是又变得愈加简单了一点?
那么前面这个“短时间内不断的调用”,用代码怎样表明呢?
很简单,便是在循环中不断的进行接口调用就行了。
publicclassMainTest{
publicstaticvoidmain(String[]args){
for(inti=0;i<1000;i++){
intfinalI=i;
newThread(()->{
HttpUtil.get("127.0.0.1:8080/getTest?num="+finalI);
}).start();
}
//堵塞主线程
Thread.yield();
}
}
当然了,这个当地你用一些压测工具,比方 jmeter 啥的,会显得逼格更高,更专业。我这儿就偷个懒,直接上代码了。
答案
通过前面的预备作业,Demo 和测验代码都就绪了。
接下来便是先把 Demo 跑起来:
然后跑一把 MainTest。
当 MainTest 跑起来之后,Demo 这边就会快速的、很多的输出这样的日志:
也便是我前面 getTest 办法中写的日志:
好,现在咱们回到这个问题:
我短时间内不断的调用这个 Demo 的 getTest 办法,最多能调用多少次?
来,请你告诉我怎样得到这个问题的答案?
我这儿便是一个大力出奇迹,直接统计“承受到恳求”关键字在日志中呈现的次数就行了:
很显然,答案便是:
所以,当面试官问你:一个 SpringBoot 项目能一起处理多少恳求?
你装作仔细考虑之后,笃定的说:200 次。
面试官微微点头,并等着你持续说下去。
你也暗自欢喜,幸亏看了歪歪歪师傅的文章,背了个答案。然后等着面试官持续问其他问题。
气氛忽然就尴尬了起来。
接着,你就回家等通知了。
200 次,这个答复是对的,可是你只说 200 次,这个答复就显得有点尬了。
重要的是,这个值是怎样来的?
所以,下面这一部分,你也要背下来。
怎样来的?
在开端探索怎样来的之前,我先问你一个问题,这个 200 个线程,是谁的线程,或许说是谁在办理这个线程?
是 SpringBoot 吗?
必定不是,SpringBoot 并不是一个 web 容器。
应该是 Tomcat 在办理这 200 个线程。
这一点,咱们通过线程 Dump 也能进行验证:
通过线程 Dump 文件,咱们能够知道,很多的线程都在 sleep 状况。而点击这些线程,检查其仓库音讯,能够看到 Tomcat、threads、ThreadPoolExecutor 等关键字:
atorg.apache.Tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1791)
atorg.apache.Tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
atorg.apache.Tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
atorg.apache.Tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
atorg.apache.Tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
根据“短时间内有 200 个恳求被立马处理的”这个现象,结合你背的滚瓜烂熟的、十分厚实的线程池知识,你先斗胆的猜一个:Tomcat 默许中心线程数是 200。
接下来,咱们便是要去源码里边验证这个猜想是否正确了。
我之前共享过阅览源码的办法,《我试图通过这篇文章,教会你一种阅览源码的办法。》,其中最重要的一条便是打一个有效的断点,然后根据断点处的调用栈去定位源码。
这儿我再教你一个不必打断点也能获取到调用栈的办法。
在前面现已展现过了,便是线程 Dump。
右边便是一个线程完整的调用栈:
从这个调用栈中,由于咱们要找的是 Tomcat 线程池相关的源码,所以榜首次呈现相关关键字的当地便是这一行:
org.apache.Tomcat.util.threads.ThreadPoolExecutor.Worker#run
然后咱们在这一行打上断点。
重启项目,开端调试。
进入 runWorker 之后,这部分代码看起来就十分眼熟了:
几乎和 JDK 里边的线程池源码一模相同。
假如你了解 JDK 线程池源码的话,调试 Tomcat 的线程池,那个感觉,就像是回家相同。
假如你不了解的话,我主张你尽快去了解了解。
随着断点往下走,在 getTask 办法里边,能够看到关于线程池的几个关键参数:
org.apache.Tomcat.util.threads.ThreadPoolExecutor#getTask
corePoolSize,中心线程数,值为 10。
maximumPoolSize,最大线程数,值为 200。
并且根据 maximumPoolSize 这个参数,你往前翻代码,会发现这个默许值便是 200:
好,到这儿,你发现你之前猜想的“Tomcat 默许中心线程数是 200”是不对的。
可是你一点也不慌,再次结合你背的滚瓜烂熟的、十分厚实的线程池知识。
并在心里又默念了一次:当线程池承受到使命之后,先启用中心线程数,再运用行列长度,最终启用最大线程数。
由于咱们前面验证了,Tomcat 能够一起间处理 200 个恳求,而它的线程池中心线程数只要 10,最大线程数是 200。
这阐明,我前面这个测验用例,把行列给塞满了,然后导致 Tomcat 线程池启用了最大线程数:
嗯,必定是这样的!
那么,现在的关键问题便是:Tomcat 线程池默许的行列长度是多少呢?
在当时的这个 Debug 形式下,行列长度能够通过 Alt+F8 进行检查:
wc,这个值是 Integer.MAX_VALUE,这么大?
我总共也才 1000 个使命,不或许被占满啊?
一个线程池:
- 中心线程数,值为 10。
- 最大线程数,值为 200。
- 行列长度,值为 Integer.MAX_VALUE。
1000 个比较耗时的使命过来之后,应该是只要 10 个线程在作业,然后剩下的 990 个进行列才对啊?
莫非我八股文背错了?
这个时分不要慌,嗦根辣条冷静一下。
现在已知的是中心线程数,值为 10。这 10 个线程的作业流程是契合咱们认知的。
可是第 11 个使命过来的时分,本应该进入行列去排队。
现在看起来,是直接启用最大线程数了。
所以,咱们先把测验用例修正一下:
那么问题就来了:最终一个恳求到底是怎样提交到线程池里边的?
前面说了,Tomcat 的线程池源码和 JDK 的基本相同。
往线程池里边提交使命的时分,会履行 execute 这个办法:
org.apache.Tomcat.util.threads.ThreadPoolExecutor#execute(java.lang.Runnable)
关于 Tomcat 它会调用到 executeInternal 这个办法:
org.apache.Tomcat.util.threads.ThreadPoolExecutor#executeInternal
这个办法里边,标号为 ① 的当地,便是判别当时作业线程数是否小于中心线程数,小于则直接调用 addWorker 办法,创立线程。
标号为 ② 的当地主要是调用了 offer 办法,看看行列里边是否还能持续增加使命。
假如不能持续增加,阐明行列满了,则来到标号为 ③ 的当地,看看是否能履行 addWorker 办法,创立非中心线程,即启用最大线程数。
把这个逻辑捋顺之后,接下来咱们应该去看哪部分的代码,就很明晰了。
主要便是去看 workQueue.offer(command) 这个逻辑。
假如回来 true 则表明加入到行列,回来 false 则表明启用最大线程数嘛。
这个 workQueue 是 TaskQueue,看起来一点也不眼熟:
当然不眼熟了,由于这个是 Tomcat 自己根据 LinkedBlockingQueue 搞的一个行列。
问题的答案就藏在 TaskQueue 的 offer 办法里边。
所以我重点带你盘一下这个 offer 办法:
org.apache.Tomcat.util.threads.TaskQueue#offer
标号为 ① 的当地,判别了 parent 是否为 null,假如是则直接调用父类的 offer 办法。阐明要启用这个逻辑,咱们的 parent 不能为 null。
那么这个 parent 是什么玩意,从哪里来的呢?
parent 便是 Tomcat 线程池,通过其 set 办法能够知道,是在线程池完成初始化之后,进行了赋值。
也便是说,你能够理解为,在 Tomcat 的场景下,parent 不会为空。
标号为 ② 的当地,调用了 getPoolSizeNoLock 办法:
这个办法是获取当时线程池中有多个线程。
所以假如这个表达式为 true:
parent.getPoolSizeNoLock() == parent.getMaximumPoolSize()
就表明当时线程池的线程数现已是装备的最大线程数了,那就调用 offer 办法,把当时恳求放到到行列里边去。
标号为 ③ 的当地,是判别现已提交到线程池里边待履行或许正在履行的使命个数,是否比当时线程池的线程数还少。
假如是,则阐明当时线程池有闲暇线程能够履行使命,则把使命放到行列里边去,就会被闲暇线程给取走履行。
然后,关键的来了,标号为 ④ 的当地。
假如当时线程池的线程数比线程池装备的最大线程数还少,则回来 false。
前面说了,offer 办法回来 false,会呈现什么状况?
是不是直接开端到上图中标号为 ③ 的当地,去测验增加非中心线程了?
也便是启用最大线程数这个装备了。
所以,朋友们,这个是什么状况?
这个状况的确就和咱们背的线程池的八股文不相同了啊。
JDK 的线程池,是先运用中心线程数装备,接着运用行列长度,最终再运用最大线程装备。
Tomcat 的线程池,便是先运用中心线程数装备,再运用最大线程装备,最终才运用行列长度。
所以,今后当面试官给你说:咱们聊聊线程池的作业机制吧?
你就先诘问一句:你是说的 JDK 的线程池呢仍是 Tomcat 的线程池呢,由于这两个在运转机制上有一点差异。
然后,你就看他的表情。
假如透露出一丝丝迟疑,然后轻描淡写的说一句:那就对比着说一下吧。
那么恭喜你,在这个题目上开端掌握了一点主动权。
最终,为了让你愈加深入的理解到 Tomcat 线程池和 JDK 线程池的不相同,我给你搞一个直接仿制曩昔就能运转的代码。
当你把 taskqueue.setParent(executor) 这行代码注释掉的时分,它的运转机制便是 JDK 的线程池。
当存在这行代码的时分,它的运转机制就变成了 Tomcat 的线程池。
玩去吧。
importorg.apache.tomcat.util.threads.TaskQueue;
importorg.apache.tomcat.util.threads.TaskThreadFactory;
importorg.apache.tomcat.util.threads.ThreadPoolExecutor;
importjava.util.concurrent.TimeUnit;
publicclassTomcatThreadPoolExecutorTest{
publicstaticvoidmain(String[]args)throwsInterruptedException{
StringnamePrefix="歪歪歪-exec-";
booleandaemon=true;
TaskQueuetaskqueue=newTaskQueue(300);
TaskThreadFactorytf=newTaskThreadFactory(namePrefix,daemon,Thread.NORM_PRIORITY);
ThreadPoolExecutorexecutor=newThreadPoolExecutor(5,
150,60000,TimeUnit.MILLISECONDS,taskqueue,tf);
taskqueue.setParent(executor);
for(inti=0;i<300;i++){
try{
executor.execute(()->{
logStatus(executor,"创立使命");
try{
TimeUnit.SECONDS.sleep(2);
}catch(InterruptedExceptione){
e.printStackTrace();
}
});
}catch(Exceptione){
e.printStackTrace();
}
}
Thread.currentThread().join();
}
privatestaticvoidlogStatus(ThreadPoolExecutorexecutor,Stringname){
TaskQueuequeue=(TaskQueue)executor.getQueue();
System.out.println(Thread.currentThread().getName()+"-"+name+"-:"+
"中心线程数:"+executor.getCorePoolSize()+
"\t活动线程数:"+executor.getActiveCount()+
"\t最大线程数:"+executor.getMaximumPoolSize()+
"\t总使命数:"+executor.getTaskCount()+
"\t当时排队线程数:"+queue.size()+
"\t行列剩下大小:"+queue.remainingCapacity());
}
}
等等
假如你之前的确没了解过 Tomcat 线程池的作业机制,那么看到这儿的时分也许你会觉得的确是有一点点收获。
可是,注意我要说可是了。
还记得最开端的时分面试官的问题吗?
面试官的原问题便是:一个 SpringBoot 项目能一起处理多少恳求?
那么请问,前面我讲了这么大一坨 Tomcat 线程池运转原理,这个答复,和这个问题匹配吗?
是的,除了最开端提出的 200 这个数值之外,并不匹配,甚至在面试官的眼里完全是答非所问了。
所以,为了把这两个“并不匹配”的东西比较顺畅的链接起来,你必须要先答复面试官的问题,然后再开端扩展。
比方这样答:一个未进行任何特殊装备,悉数采用默许设置的 SpringBoot 项目,这个项目同一时间最多能一起处理多少恳求,取决于咱们运用的 web 容器,而 SpringBoot 默许运用的是 Tomcat。
Tomcat 的默许中心线程数是 10,最大线程数 200,行列长度是无限长。可是由于其运转机制和 JDK 线程池不相同,在中心线程数满了之后,会直接启用最大线程数。所以,在默许的装备下,同一时间,能够处理 200 个恳求。
在实际运用进程中,应该根据服务实际状况和服务器装备等相关音讯,对该参数进行评价设置。
这个答复就算是差不多了。
可是,假如很不幸,假如你遇到了我,为了验证你是真的自己去探索过,仍是仅仅仅仅看了几篇文章,我或许还会诘问一下:
那么其他什么都不动,假如我仅仅加入 server.tomcat.max-connections=10 这个装备呢,那么这个时分最多能处理多少个恳求?
你或许就要猜了:10 个。
是的,我重新提交 1000 个使命过来,在操控台输出的的确是 10 个,
那么 max-connections 这个参数它怎样也能操控恳求个数呢?
为什么在前面的剖析进程中咱们并没有注意到这个参数呢?
首要咱们看一下它的默许值:
由于它的默许值是 8192,比最大线程数 200 大,这个参数并没有约束到咱们,所以咱们没有关注到它。
当咱们把它调整为 10 的时分,小于最大线程数 200,它就开端变成约束项了。
那么 max-connections 这个参数到底是干啥的呢?
你先自己去探索探索吧。
一起,还有这样的一个参数,默许是 100:
server.tomcat.accept-count=100
它又是干什么的呢?
“和连接数有关”,我只能提示到这儿了,自己去探索吧。
再等等
通过前面的剖析,咱们知道了,要答复“一个 SpringBoot 项目默许能处理的使命数”,这个问题,得先明确其运用的 web 容器。
那么问题又来了:SpringBoot 内置了哪些容器呢?
Tomcat、Jetty、Netty、Undertow
前面咱们都是根据 Tomcat 剖析的,假如咱们换一个容器呢?
比方换成 Undertow,这个玩意我仅仅听过,没有实际运用过,它对我来说便是一个黑盒。
管它的,先换了再说。
从 Tomcat 换成 Undertow,只需要修正 Maven 依赖即可,其他什么都不需要动:
再次发动项目,从日志能够发现现已修正为了 Undertow 容器:
此刻我再次履行 MainTest 办法,仍是提交 1000 个恳求:
从日志来看,发现只要 48 个恳求被处理了。
就很懵逼,48 是怎样回事儿,怎样都不是一个整数呢,这让强迫症很难受啊。
这个时分你的主意是什么,是不是想要看看 48 这个数字到底是从哪里来的?
怎样看?
之前找 Tomcat 的 200 的时分不是才教了你的嘛,直接往 Undertow 上套就行了嘛。
打线程 Dump,然后看仓库音讯:
发现 EnhancedQueueExecutor 这个线程池,接着在这个类里边去找构建线程池时的参数。
很容易就找到了这个构造办法:
所以,在这儿打上断点,重启项目。
通过 Debug 能够知道,关键参数都是从 builder 里边来的。
而 builder 里边,coreSize 和 maxSize 都是 48,行列长度是 Integer.MAX_VALUE。
所以看一下 Builder 里边的 coreSize 是怎样来的。
点过来发现 coreSize 的默许值是 16:
不要慌,再打断点,再重启项目。
然后你会在它的 setCorePoolSize 办法处停下来,而这个办法的入参便是咱们要找的 48:
顺藤摸瓜,重复几次打断点、重启的动作之后,你会找到 48 是一个名为 WORKER_TASK_CORE_THREADS 的变量,是从这儿来的:
而 WORKER_TASK_CORE_THREADS 这个变量设置的当地是这样的:
io.undertow.Undertow#start
而这儿的 workerThreads 取值是这样的:
io.undertow.Undertow.Builder#Builder
取的是机器的 CPU 个数乘以 8。
所以我这儿是 6*8=48。
哦,水落石出,原来 48 是这样来的。
没意思。
的确没意思,可是已然都现已替换为 Undertow 了,那么你去研究一下它的 NIO ByteBuffer、NIO Channel、BufferPool、XNIO Worker、IO 线程池、Worker 线程池…
然后再和 Tomcat 对比着学,
就开端有点意思了。
最终再等等
这篇文章是根据“一个 SpringBoot 项目能一起处理多少恳求?”这个面试题出发的。
可是通过咱们前面简单的剖析,你也知道,这个问题假如在没有加一些特定的前提条件的状况下,答案是各不相同的。
比方我再给你举一个例子,仍是咱们的 Demo,仅仅运用一下 @Async 注解,其他什么都不变:
再次发动项目,发起访问,日志输出变成了这样:
一起能处理的恳求,直接从 Tomcat 的默许 200 个变成了 8 个?
由于 @Async 注解对应的线程池,默许的中心线程数是 8。
之前写过这篇文章《别问了,我真的不喜爱@Async这个注解!》剖析过这个注解。
所以你看,稍微一改变,答案看起来又不相同了,一起这个恳求在内部流转的进程也不相同了,又是一个能够铺开谈的点。
在面试进程中也是这样的,不要急于答题,当你觉得面试官问题描述的不清楚的当地,你能够先试探性的问一下,看看能不能发掘出一点他没有说出来的默许条件。
当“默许条件”发掘的越多,你的答复就会更容易被面试官承受。而这个发掘的进程,也是面试进程中一个重要的表现环节。
并且,有时分,面试官就喜爱给出这样的“含糊”的问题,由于问题越含糊,坑就越多,当面试者跳进自己挖好的坑里边的时分,便是完毕一次比武的时分;当面试者看出来自己挖好的坑,并绕曩昔的时分,也是完毕一轮比武的时分。
所以,不要急于答题,多想,多问。不管是关于面试者仍是面试官,一个好的面试体会,必定不是没有互动的一问一答,而是一个彼此拉锯的进程。