<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Java 線程池配置的常見誤區(qū)

          共 6552字,需瀏覽 14分鐘

           ·

          2021-07-05 10:10

          不點(diǎn)藍(lán)字,我們哪來故事?

          每天 11 點(diǎn)更新文章,餓了點(diǎn)外賣,點(diǎn)擊 ??《無門檻外賣優(yōu)惠券,每天免費(fèi)領(lǐng)!》

          來源 | https://zhenbianshu.github.io

          前言

          由于線程的創(chuàng)建和銷毀對(duì)操作系統(tǒng)來說都是比較重量級(jí)的操作,所以線程的池化在各種語言內(nèi)都有實(shí)踐,當(dāng)然在 Java 語言中線程池是也非常重要的一部分,有 Doug Lea 大神對(duì)線程池的封裝,我們使用的時(shí)候是非常方便,但也可能會(huì)因?yàn)椴涣私馄渚唧w實(shí)現(xiàn),對(duì)線程池的配置參數(shù)存在誤解。

          我們經(jīng)常在一些技術(shù)書籍或博客上看到,向線程池提交任務(wù)時(shí),線程池的執(zhí)行邏輯如下:

          • 1、當(dāng)一個(gè)任務(wù)被提交后,線程池首先檢查正在運(yùn)行的線程數(shù)是否達(dá)到核心線程數(shù),如果未達(dá)到則創(chuàng)建一個(gè)線程。
          • 2、如果線程池內(nèi)正在運(yùn)行的線程數(shù)已經(jīng)達(dá)到了核心線程數(shù),任務(wù)將會(huì)被放到 BlockingQueue 內(nèi)。
          • 3、如果 BlockingQueue 已滿,線程池將會(huì)嘗試將線程數(shù)擴(kuò)充到最大線程池容量。
          • 4、如果當(dāng)前線程池內(nèi)線程數(shù)量已經(jīng)達(dá)到最大線程池容量,則會(huì)執(zhí)行拒絕策略拒絕任務(wù)提交。流程如圖(摘自美團(tuán)技術(shù)博客):

          Windows 11,一個(gè)新功能,一場(chǎng)新屠殺!

          流程描述沒有問題,但如果某些點(diǎn)未經(jīng)過推敲,容易導(dǎo)致誤解,而且描述中的情境太理想化,如果配置時(shí)不考慮運(yùn)行時(shí)環(huán)境,也會(huì)出現(xiàn)一些非常詭異的問題。

          核心池

          線程池內(nèi)線程數(shù)量小于等于 coreSize 的部分我稱為核心池,核心池是線程池的常駐部分,內(nèi)部的線程一般不會(huì)被銷毀,我們提交的任務(wù)也應(yīng)該絕大部分都由核心池內(nèi)的線程來執(zhí)行。


          線程創(chuàng)建時(shí)機(jī)的誤解

          有關(guān)核心池最常見的一個(gè)誤區(qū)是沒搞清楚核心池內(nèi)線程的創(chuàng)建時(shí)機(jī),這個(gè)問題,我覺得甩 10% 的鍋給 Doug Lea 大神應(yīng)該不算過分,因?yàn)樗谖臋n里寫道 “If fewer than corePoolSize threads are running, try to start a new thread with the given command as its first task”,其中 "running" 這個(gè)詞就比較有歧義,因?yàn)樵谖覀兝斫饫?running 是指當(dāng)前線程已被操作系統(tǒng)調(diào)度,擁有操作系統(tǒng)時(shí)間分片,或者被理解為正在執(zhí)行某個(gè)任務(wù)。

          基于以上的理解,我們很容易就認(rèn)為如果任務(wù)的 QPS 非常低,線程池內(nèi)線程數(shù)量永遠(yuǎn)也達(dá)不到 coreSize。即如果我們配置了 coreSize 為 1000,實(shí)際上 QPS 只有 1,單個(gè)任務(wù)耗時(shí) 1s,那么核心池大小就會(huì)一直是 1,即使有流量抖動(dòng),核心池也只會(huì)被擴(kuò)容到 3。因?yàn)橐粋€(gè)線程每秒執(zhí)行執(zhí)行一個(gè)任務(wù),剛好不用創(chuàng)建新線程就足以應(yīng)對(duì) 1QPS。

          創(chuàng)建過程

          但如果簡(jiǎn)單設(shè)計(jì)一個(gè)測(cè)試,使用 jstack 打印出線程棧并數(shù)一下線程池內(nèi)線程數(shù)量,會(huì)發(fā)現(xiàn)線程池內(nèi)的線程數(shù)會(huì)隨著任務(wù)的提交而逐漸增大,直到達(dá)到 coreSize。

          因?yàn)楹诵某氐脑O(shè)計(jì)初衷是想它能作為常駐池,承載日常流量,所以它應(yīng)該被盡快初始化,于是線程池的邏輯是在沒有達(dá)到 coreSize 之前,每一個(gè)任務(wù)都會(huì)創(chuàng)建一個(gè)新的線程,對(duì)應(yīng)的源碼為:

              public void execute(Runnable command) {
                  ...
                  int c = ctl.get();
                  if (workerCountOf(c) < corePoolSize) { // workerCountOf() 方法是獲取線程池內(nèi)線程數(shù)量
                      if (addWorker(command, true))
                          return;
                      c = ctl.get();
                  }
                  ...
              }

          而文檔里的 running 狀態(tài)也指的是線程已經(jīng)被創(chuàng)建,我們也知道線程被創(chuàng)建后,會(huì)在一個(gè) while 循環(huán)里嘗試從 BlockingQueue 里獲取并執(zhí)行任務(wù),說它正在 running 也不為過。

          基于此,我們對(duì)一些高并發(fā)服務(wù)進(jìn)行的預(yù)熱,其實(shí)并不是期望 JVM 能對(duì)熱點(diǎn)代碼做 JIT 等優(yōu)化,對(duì)線程池、連接池和本地緩存的預(yù)熱才是重點(diǎn)。

          BlockingQueue

          BlockingQueue 是線程池內(nèi)的另一個(gè)重要組件,首先它是線程池”生產(chǎn)者-消費(fèi)者”模型的中間媒介,另外它也可以為大量突發(fā)的流量做緩沖,但理解和配置它也經(jīng)常會(huì)出錯(cuò)。

          運(yùn)行模型

          最常見的錯(cuò)誤是不理解線程池的運(yùn)行模型。首先要明確的一點(diǎn)是線程池并沒有準(zhǔn)確的調(diào)度功能,即它無法感知有哪些線程是處于空閑狀態(tài)的,并把提交的任務(wù)派發(fā)給空閑線程。線程池采用的是”生產(chǎn)者-消費(fèi)者”模式,除了觸發(fā)線程創(chuàng)建的任務(wù)(線程的 firstTask)不會(huì)入 BlockingQueue 外,其他任務(wù)都要進(jìn)入到 BlockingQueue,等待線程池內(nèi)的線程消費(fèi),而任務(wù)會(huì)被哪個(gè)線程消費(fèi)到完全取決于操作系統(tǒng)的調(diào)度。

          對(duì)應(yīng)的生產(chǎn)者源碼如下:

              public void execute(Runnable command) {
                  ...
                  if (isRunning(c) && workQueue.offer(command)) { isRunning() 是判斷線程池處理戚狀態(tài)
                      int recheck = ctl.get();
                      if (! isRunning(recheck) && remove(command))
                          reject(command);
                      else if (workerCountOf(recheck) == 0)
                          addWorker(nullfalse);
                  }
                  ...
              }

          對(duì)應(yīng)的消費(fèi)者源碼如下:

          private Runnable getTask() {
                  for (;;) {
                      ...
                      Runnable r = timed ?
                          workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                          workQueue.take();
                      if (r != null)
                          return r;
                      ...
                  }
              }

          BlockingQueue 的緩沖作用

          基于”生產(chǎn)者-消費(fèi)者”模型,我們可能會(huì)認(rèn)為如果配置了足夠的消費(fèi)者,線程池就不會(huì)有任何問題。其實(shí)不然,我們還必須考慮并發(fā)量這一因素。

          設(shè)想以下情況:有 1000 個(gè)任務(wù)要同時(shí)提交到線程池內(nèi)并發(fā)執(zhí)行,在線程池被初始化完成的情況下,它們都要被放到 BlockingQueue 內(nèi)等待被消費(fèi),在極限情況下,消費(fèi)線程一個(gè)任務(wù)也沒有執(zhí)行完成,那么這 1000 個(gè)請(qǐng)求需要同時(shí)存在于 BlockingQueue 內(nèi),如果配置的 BlockingQueue Size 小于 1000,多余的請(qǐng)求就會(huì)被拒絕。

          那么這種極限情況發(fā)生的概率有多大呢?答案是非常大,因?yàn)椴僮飨到y(tǒng)對(duì) I/O 線程的調(diào)度優(yōu)先級(jí)是非常高的,一般我們的任務(wù)都是由 I/O 的準(zhǔn)備或完成(如 tomcat 受理了 http 請(qǐng)求)開始的,所以很有可能被調(diào)度到的都是 tomcat 線程,它們?cè)谝恢蓖€程池內(nèi)提交請(qǐng)求,而消費(fèi)者線程卻調(diào)度不到,導(dǎo)致請(qǐng)求堆積。

          我負(fù)責(zé)的服務(wù)就發(fā)生過這種請(qǐng)求被異常拒絕的情況,壓測(cè)時(shí) QPS 2000,平均響應(yīng)時(shí)間為 20ms,正常情況下,40 個(gè)線程就可以平衡生產(chǎn)速度,不會(huì)堆積。但在 BlockingQueue Size 為 50 時(shí),即使線程池 coreSize 為 1000,還會(huì)出現(xiàn)請(qǐng)求被線程池拒絕的情況。

          這種情況下,BlockingQueue 的重要的意義就是它是一個(gè)能長(zhǎng)時(shí)間存儲(chǔ)任務(wù)的容器,能以很小的代價(jià)為線程池提供緩沖。根據(jù)上文可知,線程池能支持BlockingQueue Size個(gè)任務(wù)同時(shí)提交,我們把最大同時(shí)提交的任務(wù)個(gè)數(shù),稱為并發(fā)量,配置線程池時(shí),了解并發(fā)量異常重要。

          并發(fā)量的計(jì)算

          我們常用 QPS 來衡量服務(wù)壓力,所以配置線程池參數(shù)時(shí)也經(jīng)常參考這個(gè)值,但有時(shí)候 QPS 和并發(fā)量有時(shí)候相關(guān)性并沒有那么高,QPS 還要搭配任務(wù)執(zhí)行時(shí)間來推算峰值并發(fā)量。

          比如請(qǐng)求間隔嚴(yán)格相同的接口,平均 QPS 為 1000,它的并發(fā)量峰值是多少呢?我們并沒有辦法估算,因?yàn)槿绻蝿?wù)執(zhí)行時(shí)間為 1ms,那么它的并發(fā)量只有 1;而如果任務(wù)執(zhí)行時(shí)間為 1s,那么并發(fā)量峰值為 1000。

          可是知道了任務(wù)執(zhí)行時(shí)間,就能算出并發(fā)量了嗎?也不能,因?yàn)槿绻?qǐng)求的間隔不同,可能 1min 內(nèi)的請(qǐng)求都在一秒內(nèi)發(fā)過來,那這個(gè)并發(fā)量還要乘以 60,所以上面才說知道了 QPS 和任務(wù)執(zhí)行時(shí)間,并發(fā)量也只能靠推算。

          計(jì)算并發(fā)量,我一般的經(jīng)驗(yàn)值是 QPS * 平均響應(yīng)時(shí)間,再留上一倍的冗余,但如果業(yè)務(wù)重要的話,BlockingQueue Size 設(shè)置大一些也無妨(1000 或以上),畢竟每個(gè)任務(wù)占用的內(nèi)存量很有限。

          考慮運(yùn)行時(shí)

          GC

          除了上面提到的各種情況下,GC 也是一個(gè)很重要的影響因素。

          我們都知道 GC 是 Stop the World 的,但這里的 World 指的是 JVM,而一個(gè)請(qǐng)求 I/O 的準(zhǔn)備和完成是操作系統(tǒng)在進(jìn)行的,JVM 停止了,但操作系統(tǒng)還是會(huì)正常受理請(qǐng)求,在 JVM 恢復(fù)后執(zhí)行,所以 GC 是會(huì)堆積請(qǐng)求的。

          上文中提到的并發(fā)量計(jì)算一定要考慮到 GC 時(shí)間內(nèi)堆積的請(qǐng)求同時(shí)被受理的情況,堆積的請(qǐng)求數(shù)可以通過 QPS * GC時(shí)間 來簡(jiǎn)單得出,還有一定要記得留出冗余。

          業(yè)務(wù)峰值

          除此之外,配置線程池參數(shù)時(shí),一定要考慮業(yè)務(wù)場(chǎng)景。

          假如接口的流量大部分來自于一個(gè)定時(shí)程序,那么平均 QPS 就沒有了任何意義,線程池設(shè)計(jì)時(shí)就要考慮給 BlockingQueue 的 Size 設(shè)置一個(gè)大一些的值;而如果流量非常不平均,一天內(nèi)只有某一小段時(shí)間才有高流量的話,而且線程資源緊張的情況下,就要考慮給線程池的 maxSize 留下較大的冗余;在流量尖刺明顯而響應(yīng)時(shí)間不那么敏感時(shí),也可以設(shè)置較大的 BlockingQueue,允許任務(wù)進(jìn)行一定程度的堆積。

          當(dāng)然除了經(jīng)驗(yàn)和計(jì)算外,對(duì)服務(wù)做定時(shí)的壓測(cè)無疑更能幫助掌握服務(wù)真實(shí)的情況。

          小結(jié)

          總結(jié)線程池的配置時(shí),我最大的感受是一定要讀源碼!讀源碼!讀源碼!只看一些書和文章的總結(jié)是無法吃透一些重要概念的,即使搞懂了大部分也很容易會(huì)在一些角落踩坑。深入理解原理后,面對(duì)復(fù)雜情況,才有靈活配置的能力。

          往期推薦

          快手宣布取消“大小周”,互聯(lián)網(wǎng)公司“996風(fēng)氣”封印松動(dòng)?

          來瞅一瞅Spring Boot的多種漏洞!

          用 Google Guava 編程強(qiáng)過阿里巴巴規(guī)范?

          100 億次的請(qǐng)求怎么抗?

          下方二維碼關(guān)注我

          技術(shù)草根堅(jiān)持分享 編程,算法,架構(gòu)

          看完文章,餓了點(diǎn)外賣,點(diǎn)擊 ??《無門檻外賣優(yōu)惠券,每天免費(fèi)領(lǐng)!》

          朋友,助攻一把!點(diǎn)個(gè)在看


          瀏覽 61
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  在线操比| 日韩探花| 一级福利在线播放 | 美女被日网站 | 黄色一级片A |