<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      面試官:一個 SpringBoot 項目能處理多少請求?(小心有坑)

      你好呀,我是歪歪。

      這篇文章帶大家盤一個讀者遇到的面試題哈。

      根據讀者轉述,面試官的原問題就是:一個 SpringBoot 項目能同時處理多少請求?

      不知道你聽到這個問題之后的第一反應是什么。

      我大概知道他要問的是哪個方向,但是對于這種只有一句話的面試題,我的第一反應是:會不會有坑?

      所以并不會貿然答題,先追問一些消息,比如:這個項目具體是干什么的?項目大概進行了哪些參數配置?使用的 web 容器是什么?部署的服務器配置如何?有哪些接口?接口響應平均時間大概是多少?

      這樣,在幾個問題的拉扯之后,至少在面試題考察的方向方面能基本和面試官達成了一致。

      比如前面的面試問題,經過幾次拉扯之后,面試官可能會修改為:

      一個 SpringBoot 項目,未進行任何特殊配置,全部采用默認設置,這個項目同一時刻,最多能同時處理多少請求?

      能處理多少呢?

      我也不知道,但是當問題變成上面這樣之后,我找到了探索答案的角度。

      既然“未進行任何特殊配置”,那我自己搞個 Demo 出來,壓一把不就完事了嗎?

      坐穩扶好,準備發車。

      Demo

      小手一抖,先搞個 Demo 出來。

      這個 Demo 非常的簡單,就是通過 idea 創建一個全新的 SpringBoot 項目就行。

      我的 SpringBoot 版本使用的是 2.7.13。

      整個項目只有這兩個依賴:

      整個項目也只有兩個類,要得就是一個空空如也,一清二白。

      項目中的 TestController,里面只有一個 getTest 方法,用來測試,方法里面接受到請求之后直接 sleep 一小時。

      目的就是直接把當前請求線程占著,這樣我們才能知道項目中一共有多少個線程可以使用:

      @Slf4j
      @RestController
      public class TestController {

          @GetMapping("/getTest")
          public void getTest(int num) throws Exception {
              log.info("{} 接受到請求:num={}", Thread.currentThread().getName(), num);
              TimeUnit.HOURS.sleep(1);
          }
      }

      項目中的 application.properties 文件也是空的:

      這樣,一個“未進行任何特殊配置”的 SpringBoot 不就有了嗎?

      基于這個 Demo,前面的面試題就要變成了:我短時間內不斷的調用這個 Demo 的 getTest 方法,最多能調用多少次?

      問題是不是又變得更加簡單了一點?

      那么前面這個“短時間內不斷的調用”,用代碼怎么表示呢?

      很簡單,就是在循環中不斷的進行接口調用就行了。

      public class MainTest {
          public static void main(String[] args) {
              for (int i = 0; i < 1000; i++) {
                  int finalI = i;
                  new Thread(() -> {
                      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 等關鍵字:

      at org.apache.Tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1791)
      at org.apache.Tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
      at org.apache.Tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
      at org.apache.Tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
      at org.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 的線程池。

      玩去吧。

      import org.apache.tomcat.util.threads.TaskQueue;
      import org.apache.tomcat.util.threads.TaskThreadFactory;
      import org.apache.tomcat.util.threads.ThreadPoolExecutor;

      import java.util.concurrent.TimeUnit;

      public class TomcatThreadPoolExecutorTest {

          public static void main(String[] args) throws InterruptedException {
              String namePrefix = "歪歪歪-exec-";
              boolean daemon = true;
              TaskQueue taskqueue = new TaskQueue(300);
              TaskThreadFactory tf = new TaskThreadFactory(namePrefix, daemon, Thread.NORM_PRIORITY);
              ThreadPoolExecutor executor = new ThreadPoolExecutor(5,
                      150, 60000, TimeUnit.MILLISECONDS, taskqueue, tf);
              taskqueue.setParent(executor);
              for (int i = 0; i < 300; i++) {
                  try {
                      executor.execute(() -> {
                          logStatus(executor, "創建任務");
                          try {
                              TimeUnit.SECONDS.sleep(2);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      });
                  } catch (Exception e) {
                      e.printStackTrace();
                  }
              }
              Thread.currentThread().join();
          }

          private static void logStatus(ThreadPoolExecutor executor, String name) {
              TaskQueue queue = (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這個注解!》分析過這個注解。

      所以你看,稍微一變化,答案看起來又不一樣了,同時這個請求在內部流轉的過程也不一樣了,又是一個可以鋪開談的點。

      在面試過程中也是這樣的,不要急于答題,當你覺得面試官問題描述的不清楚的地方,你可以先試探性的問一下,看看能不能挖掘出一點他沒有說出來的默認條件。

      當“默認條件”挖掘的越多,你的回答就會更容易被面試官接受。而這個挖掘的過程,也是面試過程中一個重要的表現環節。

      而且,有時候,面試官就喜歡給出這樣的“模糊”的問題,因為問題越模糊,坑就越多,當面試者跳進自己挖好的坑里面的時候,就是結束一次交鋒的時候;當面試者看出來自己挖好的坑,并繞過去的時候,也是結束一輪交鋒的時候。

      所以,不要急于答題,多想,多問。不管是對于面試者還是面試官,一個好的面試體驗,一定不是沒有互動的一問一答,而是一個相互拉鋸的過程。

      posted @ 2023-07-17 13:01  why技術  閱讀(24702)  評論(54)    收藏  舉報
      主站蜘蛛池模板: 国内精品久久久久影院日本| 人妻熟女av一区二区三区| 乱人伦人妻中文字幕无码久久网| 日韩精品中文字幕国产一| 免费观看添你到高潮视频| 亚洲综合av一区二区三区| 日日碰狠狠添天天爽五月婷| 精品九九人人做人人爱| 丰满人妻一区二区三区无码AV| 国产综合欧美| 成人精品色一区二区三区| 日韩精品 在线 国产 丝袜| 国产成人精品a视频| 炉霍县| 日本人一区二区在线观看| 中文字幕在线国产精品| 99久久婷婷国产综合精品青草漫画 | 男女裸交免费无遮挡全过程| 久久天天躁夜夜躁狠狠综合| 99亚洲男女激情在线观看| 精品国产中文字幕懂色| 内射中出无码护士在线| 亚洲精品在线少妇内射| 一出一进一爽一粗一大视频| 天美传媒一区二区| 成人精品一区二区三区四| 美乳丰满人妻无码视频| 国产无套护士在线观看| 91偷自国产一区二区三区| 国产精品久久久久鬼色| 美日韩av一区二区三区| 午夜成人无码免费看网站| 亚洲欧美自偷自拍视频图片| аⅴ天堂国产最新版在线中文| 一级片免费网站| 婷婷99视频精品全部在线观看| 亚州少妇无套内射激情视频| 啦啦啦视频在线日韩精品| 成人中文在线| 狠狠色噜噜狠狠狠狠色综合久| 永久免费在线观看蜜桃视频|