<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 線程池配置的常見(jiàn)誤區(qū)

          共 1389字,需瀏覽 3分鐘

           ·

          2021-11-18 10:39

          來(lái)源:https://zhenbianshu.github.io

          前言

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

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

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

          流程如圖(摘自美團(tuán)技術(shù)博客):

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

          核心池

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

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

          有關(guān)核心池最常見(jiàn)的一個(gè)誤區(qū)是沒(méi)搞清楚核心池內(nèi)線程的創(chuàng)建時(shí)機(jī),這個(gè)問(wèn)題,我覺(jué)得甩 10% 的鍋給 Doug Lea 大神應(yīng)該不算過(guò)分,因?yàn)樗谖臋n里寫(xiě)道 “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)建過(guò)程

          但如果簡(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)該被盡快初始化,于是線程池的邏輯是在沒(méi)有達(dá)到 coreSize 之前,每一個(gè)任務(wù)都會(huì)創(chuàng)建一個(gè)新的線程,對(duì)應(yīng)的源碼為:

          public?void?execute(Runnable?command)?{
          ????...
          ????int?c?=?ctl.get();
          ????if?(workerCountOf(c)?//?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ù),說(shuō)它正在 running 也不為過(guò)。

          基于此,我們對(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)行模型

          最常見(jiàn)的錯(cuò)誤是不理解線程池的運(yùn)行模型。首先要明確的一點(diǎn)是線程池并沒(méi)有準(zhǔn)確的調(diào)度功能,即它無(wú)法感知有哪些線程是處于空閑狀態(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(null,?false);
          ????}
          ????...
          }

          對(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ì)有任何問(wèn)題。其實(shí)不然,我們還必須考慮并發(fā)量這一因素。

          設(shè)想以下情況:有 1000 個(gè)任務(wù)要同時(shí)提交到線程池內(nèi)并發(fā)執(zhí)行,在線程池被初始化完成的情況下,它們都要被放到 BlockingQueue 內(nèi)等待被消費(fèi),在極限情況下,消費(fèi)線程一個(gè)任務(wù)也沒(méi)有執(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)求)開(kāi)始的,所以很有可能被調(diào)度到的都是 tomcat 線程,它們?cè)谝恢蓖€程池內(nèi)提交請(qǐng)求,而消費(fèi)者線程卻調(diào)度不到,導(dǎo)致請(qǐng)求堆積。

          我負(fù)責(zé)的服務(wù)就發(fā)生過(guò)這種請(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ù),稱(chēng)為并發(fā)量,配置線程池時(shí),了解并發(fā)量異常重要。

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

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

          比如請(qǐng)求間隔嚴(yán)格相同的接口,平均 QPS 為 1000,它的并發(fā)量峰值是多少呢?我們并沒(méi)有辦法估算,因?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ā)過(guò)來(lái),那這個(gè)并發(fā)量還要乘以 60,所以上面才說(shuō)知道了 QPS 和任務(wù)執(zhí)行時(shí)間,并發(fā)量也只能靠推算。

          計(jì)算并發(fā)量,我一般的經(jīng)驗(yàn)值是 QPS*平均響應(yīng)時(shí)間,再留上一倍的冗余,但如果業(yè)務(wù)重要的話,BlockingQueue Size 設(shè)置大一些也無(wú)妨(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ù)可以通過(guò) QPS*GC時(shí)間 來(lái)簡(jiǎn)單得出,還有一定要記得留出冗余。

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

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

          假如接口的流量大部分來(lái)自于一個(gè)定時(shí)程序,那么平均 QPS 就沒(méi)有了任何意義,線程池設(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ú)疑更能幫助掌握服務(wù)真實(shí)的情況。

          小結(jié)

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

          參考文獻(xiàn):

          Java線程池實(shí)現(xiàn)原理及其在美團(tuán)業(yè)務(wù)中的實(shí)踐

          程序汪資料鏈接

          程序汪接的7個(gè)私活都在這里,經(jīng)驗(yàn)整理

          Java項(xiàng)目分享 最新整理全集,找項(xiàng)目不累啦 06版

          堪稱(chēng)神級(jí)的Spring Boot手冊(cè),從基礎(chǔ)入門(mén)到實(shí)戰(zhàn)進(jìn)階

          臥槽!字節(jié)跳動(dòng)《算法中文手冊(cè)》火了,完整版 PDF 開(kāi)放下載!

          臥槽!阿里大佬總結(jié)的《圖解Java》火了,完整版PDF開(kāi)放下載!

          字節(jié)跳動(dòng)總結(jié)的設(shè)計(jì)模式 PDF 火了,完整版開(kāi)放下載!


          歡迎添加程序汪個(gè)人微信 itwang009? 進(jìn)粉絲群或圍觀朋友圈!

          瀏覽 24
          點(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>
                  欧美 高潮喷高清 | 视频一区二区三区四 | 国产9在线观看黄A片免费 | 多国五级毛片 | 操操操干干干 |