隨著計(jì)算機(jī)行業(yè)的飛速發(fā)展像街,摩爾定律逐漸失效,多核CPU成為主流晋渺。使用多線程并行計(jì)算逐漸成為開發(fā)人員提升服務(wù)器性能的基本武器宅广。J.U.C提供的線程池:ThreadPoolExecutor類,幫助開發(fā)人員管理線程并方便地執(zhí)行并行任務(wù)些举。了解并合理使用線程池,是一個(gè)開發(fā)人員必修的基本功俭厚。
本文開篇簡述線程池概念和用途户魏,接著結(jié)合線程池的源碼,幫助讀者領(lǐng)略線程池的設(shè)計(jì)思路挪挤,最后回歸實(shí)踐叼丑,通過案例講述使用線程池遇到的問題,并給出了一種動(dòng)態(tài)化線程池解決方案扛门。
一鸠信、寫在前面
1.1 線程池是什么
線程池(Thread Pool)是一種基于池化思想管理線程的工具谜慌,經(jīng)常出現(xiàn)在多線程服務(wù)器中扁达,如MySQL。
線程過多會帶來額外的開銷,其中包括創(chuàng)建銷毀線程的開銷蛙卤、調(diào)度線程的開銷等等,同時(shí)也降低了計(jì)算機(jī)的整體性能键菱。線程池維護(hù)多個(gè)線程顺饮,等待監(jiān)督管理者分配可并發(fā)執(zhí)行的任務(wù)。這種做法劲装,一方面避免了處理任務(wù)時(shí)創(chuàng)建銷毀線程開銷的代價(jià)胧沫,另一方面避免了線程數(shù)量膨脹導(dǎo)致的過分調(diào)度問題,保證了對內(nèi)核的充分利用占业。
而本文描述線程池是JDK中提供的ThreadPoolExecutor類绒怨。
當(dāng)然,使用線程池可以帶來一系列好處:
- 降低資源消耗:通過池化技術(shù)重復(fù)利用已創(chuàng)建的線程谦疾,降低線程創(chuàng)建和銷毀造成的損耗南蹂。
- 提高響應(yīng)速度:任務(wù)到達(dá)時(shí),無需等待線程創(chuàng)建即可立即執(zhí)行餐蔬。
- 提高線程的可管理性:線程是稀缺資源碎紊,如果無限制創(chuàng)建,不僅會消耗系統(tǒng)資源樊诺,還會因?yàn)榫€程的不合理分布導(dǎo)致資源調(diào)度失衡仗考,降低系統(tǒng)的穩(wěn)定性。使用線程池可以進(jìn)行統(tǒng)一的分配词爬、調(diào)優(yōu)和監(jiān)控秃嗜。
- 提供更多更強(qiáng)大的功能:線程池具備可拓展性,允許開發(fā)人員向其中增加更多的功能顿膨。比如延時(shí)定時(shí)線程池ScheduledThreadPoolExecutor锅锨,就允許任務(wù)延期執(zhí)行或定期執(zhí)行。
1.2 線程池解決的問題是什么
線程池解決的核心問題就是資源管理問題恋沃。在并發(fā)環(huán)境下必搞,系統(tǒng)不能夠確定在任意時(shí)刻中,有多少任務(wù)需要執(zhí)行囊咏,有多少資源需要投入恕洲。這種不確定性將帶來以下若干問題:
- 頻繁申請/銷毀資源和調(diào)度資源,將帶來額外的消耗梅割,可能會非常巨大霜第。
- 對資源無限申請缺少抑制手段,易引發(fā)系統(tǒng)資源耗盡的風(fēng)險(xiǎn)户辞。
- 系統(tǒng)無法合理管理內(nèi)部的資源分布泌类,會降低系統(tǒng)的穩(wěn)定性。
為解決資源分配這個(gè)問題底燎,線程池采用了“池化”(Pooling)思想刃榨。池化弹砚,顧名思義,是為了最大化收益并最小化風(fēng)險(xiǎn)喇澡,而將資源統(tǒng)一在一起管理的一種思想迅栅。
Pooling is the grouping together of resources (assets, equipment, personnel, effort, etc.) for the purposes of maximizing advantage or minimizing risk to the users. The term is used in finance, computing and equipment management.——wikipedia
“池化”思想不僅僅能應(yīng)用在計(jì)算機(jī)領(lǐng)域,在金融晴玖、設(shè)備读存、人員管理、工作管理等領(lǐng)域也有相關(guān)的應(yīng)用呕屎。
在計(jì)算機(jī)領(lǐng)域中的表現(xiàn)為:統(tǒng)一管理IT資源让簿,包括服務(wù)器、存儲秀睛、和網(wǎng)絡(luò)資源等等尔当。通過共享資源,使用戶在低投入中獲益蹂安。除去線程池椭迎,還有其他比較典型的幾種使用策略包括:
- 內(nèi)存池(Memory Pooling):預(yù)先申請內(nèi)存,提升申請內(nèi)存速度田盈,減少內(nèi)存碎片畜号。
- 連接池(Connection Pooling):預(yù)先申請數(shù)據(jù)庫連接,提升申請連接的速度允瞧,降低系統(tǒng)的開銷简软。
- 實(shí)例池(Object Pooling):循環(huán)使用對象,減少資源在初始化和釋放時(shí)的昂貴損耗述暂。
在了解完“是什么”和“為什么”之后痹升,下面我們來一起深入一下線程池的內(nèi)部實(shí)現(xiàn)原理。
二畦韭、線程池核心設(shè)計(jì)與實(shí)現(xiàn)
在前文中疼蛾,我們了解到:線程池是一種通過“池化”思想,幫助我們管理線程而獲取并發(fā)性的工具艺配,在Java中的體現(xiàn)是ThreadPoolExecutor類据过。那么它的的詳細(xì)設(shè)計(jì)與實(shí)現(xiàn)是什么樣的呢?我們會在本章進(jìn)行詳細(xì)介紹妒挎。
2.1 總體設(shè)計(jì)
Java中的線程池核心實(shí)現(xiàn)類是ThreadPoolExecutor,本章基于JDK 1.8的源碼來分析Java線程池的核心設(shè)計(jì)與實(shí)現(xiàn)西饵。我們首先來看一下ThreadPoolExecutor的UML類圖酝掩,了解下ThreadPoolExecutor的繼承關(guān)系。
圖1 ThreadPoolExecutor UML類圖
ThreadPoolExecutor實(shí)現(xiàn)的頂層接口是Executor眷柔,頂層接口Executor提供了一種思想:將任務(wù)提交和任務(wù)執(zhí)行進(jìn)行解耦期虾。用戶無需關(guān)注如何創(chuàng)建線程原朝,如何調(diào)度線程來執(zhí)行任務(wù),用戶只需提供Runnable對象镶苞,將任務(wù)的運(yùn)行邏輯提交到執(zhí)行器(Executor)中喳坠,由Executor框架完成線程的調(diào)配和任務(wù)的執(zhí)行部分。ExecutorService接口增加了一些能力:(1)擴(kuò)充執(zhí)行任務(wù)的能力茂蚓,補(bǔ)充可以為一個(gè)或一批異步任務(wù)生成Future的方法壕鹉;(2)提供了管控線程池的方法,比如停止線程池的運(yùn)行聋涨。AbstractExecutorService則是上層的抽象類晾浴,將執(zhí)行任務(wù)的流程串聯(lián)了起來,保證下層的實(shí)現(xiàn)只需關(guān)注一個(gè)執(zhí)行任務(wù)的方法即可牍白。最下層的實(shí)現(xiàn)類ThreadPoolExecutor實(shí)現(xiàn)最復(fù)雜的運(yùn)行部分脊凰,ThreadPoolExecutor將會一方面維護(hù)自身的生命周期,另一方面同時(shí)管理線程和任務(wù)茂腥,使兩者良好的結(jié)合從而執(zhí)行并行任務(wù)狸涌。
ThreadPoolExecutor是如何運(yùn)行,如何同時(shí)維護(hù)線程和執(zhí)行任務(wù)的呢最岗?其運(yùn)行機(jī)制如下圖所示:
圖2 ThreadPoolExecutor運(yùn)行流程
線程池在內(nèi)部實(shí)際上構(gòu)建了一個(gè)生產(chǎn)者消費(fèi)者模型帕胆,將線程和任務(wù)兩者解耦,并不直接關(guān)聯(lián)仑性,從而良好的緩沖任務(wù)惶楼,復(fù)用線程。線程池的運(yùn)行主要分成兩部分:任務(wù)管理诊杆、線程管理歼捐。任務(wù)管理部分充當(dāng)生產(chǎn)者的角色,當(dāng)任務(wù)提交后晨汹,線程池會判斷該任務(wù)后續(xù)的流轉(zhuǎn):(1)直接申請線程執(zhí)行該任務(wù)豹储;(2)緩沖到隊(duì)列中等待線程執(zhí)行;(3)拒絕該任務(wù)淘这。線程管理部分是消費(fèi)者剥扣,它們被統(tǒng)一維護(hù)在線程池內(nèi),根據(jù)任務(wù)請求進(jìn)行線程的分配铝穷,當(dāng)線程執(zhí)行完任務(wù)后則會繼續(xù)獲取新的任務(wù)去執(zhí)行钠怯,最終當(dāng)線程獲取不到任務(wù)的時(shí)候,線程就會被回收曙聂。
接下來晦炊,我們會按照以下三個(gè)部分去詳細(xì)講解線程池運(yùn)行機(jī)制:
- 線程池如何維護(hù)自身狀態(tài)。
- 線程池如何管理任務(wù)。
- 線程池如何管理線程断国。
2.2 生命周期管理
線程池運(yùn)行的狀態(tài)贤姆,并不是用戶顯式設(shè)置的,而是伴隨著線程池的運(yùn)行稳衬,由內(nèi)部來維護(hù)霞捡。線程池內(nèi)部使用一個(gè)變量維護(hù)兩個(gè)值:運(yùn)行狀態(tài)(runState)和線程數(shù)量 (workerCount)。在具體實(shí)現(xiàn)中薄疚,線程池將運(yùn)行狀態(tài)(runState)碧信、線程數(shù)量 (workerCount)兩個(gè)關(guān)鍵參數(shù)的維護(hù)放在了一起,如下代碼所示:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
ctl
這個(gè)AtomicInteger類型输涕,是對線程池的運(yùn)行狀態(tài)和線程池中有效線程的數(shù)量進(jìn)行控制的一個(gè)字段音婶, 它同時(shí)包含兩部分的信息:線程池的運(yùn)行狀態(tài) (runState) 和線程池內(nèi)有效線程的數(shù)量 (workerCount),高3位保存runState莱坎,低29位保存workerCount衣式,兩個(gè)變量之間互不干擾。用一個(gè)變量去存儲兩個(gè)值檐什,可避免在做相關(guān)決策時(shí)碴卧,出現(xiàn)不一致的情況,不必為了維護(hù)兩者的一致乃正,而占用鎖資源住册。通過閱讀線程池源代碼也可以發(fā)現(xiàn),經(jīng)常出現(xiàn)要同時(shí)判斷線程池運(yùn)行狀態(tài)和線程數(shù)量的情況瓮具。線程池也提供了若干方法去供用戶獲得線程池當(dāng)前的運(yùn)行狀態(tài)荧飞、線程個(gè)數(shù)。這里都使用的是位運(yùn)算的方式名党,相比于基本運(yùn)算叹阔,速度也會快很多。
關(guān)于內(nèi)部封裝的獲取生命周期狀態(tài)传睹、獲取線程池線程數(shù)量的計(jì)算方法如以下代碼所示:
private static int runStateOf(int c) { return c & ~CAPACITY; } //計(jì)算當(dāng)前運(yùn)行狀態(tài)
private static int workerCountOf(int c) { return c & CAPACITY; } //計(jì)算當(dāng)前線程數(shù)量
private static int ctlOf(int rs, int wc) { return rs | wc; } //通過狀態(tài)和線程數(shù)生成ctl
ThreadPoolExecutor的運(yùn)行狀態(tài)有5種耳幢,分別為:
其生命周期轉(zhuǎn)換如下入所示:
圖3 線程池生命周期
2.3 任務(wù)執(zhí)行機(jī)制
2.3.1 任務(wù)調(diào)度
任務(wù)調(diào)度是線程池的主要入口,當(dāng)用戶提交了一個(gè)任務(wù)欧啤,接下來這個(gè)任務(wù)將如何執(zhí)行都是由這個(gè)階段決定的睛藻。了解這部分就相當(dāng)于了解了線程池的核心運(yùn)行機(jī)制。
首先邢隧,所有任務(wù)的調(diào)度都是由execute方法完成的店印,這部分完成的工作是:檢查現(xiàn)在線程池的運(yùn)行狀態(tài)、運(yùn)行線程數(shù)倒慧、運(yùn)行策略按摘,決定接下來執(zhí)行的流程讥邻,是直接申請線程執(zhí)行,或是緩沖到隊(duì)列中執(zhí)行院峡,亦或是直接拒絕該任務(wù)。其執(zhí)行過程如下:
- 首先檢測線程池運(yùn)行狀態(tài)系宜,如果不是RUNNING照激,則直接拒絕,線程池要保證在RUNNING的狀態(tài)下執(zhí)行任務(wù)盹牧。
- 如果workerCount < corePoolSize俩垃,則創(chuàng)建并啟動(dòng)一個(gè)線程來執(zhí)行新提交的任務(wù)。
- 如果workerCount >= corePoolSize汰寓,且線程池內(nèi)的阻塞隊(duì)列未滿口柳,則將任務(wù)添加到該阻塞隊(duì)列中。
- 如果workerCount >= corePoolSize && workerCount < maximumPoolSize有滑,且線程池內(nèi)的阻塞隊(duì)列已滿跃闹,則創(chuàng)建并啟動(dòng)一個(gè)線程來執(zhí)行新提交的任務(wù)。
- 如果workerCount >= maximumPoolSize毛好,并且線程池內(nèi)的阻塞隊(duì)列已滿, 則根據(jù)拒絕策略來處理該任務(wù), 默認(rèn)的處理方式是直接拋異常望艺。
其執(zhí)行流程如下圖所示:
圖4 任務(wù)調(diào)度流程
2.3.2 任務(wù)緩沖
任務(wù)緩沖模塊是線程池能夠管理任務(wù)的核心部分。線程池的本質(zhì)是對任務(wù)和線程的管理肌访,而做到這一點(diǎn)最關(guān)鍵的思想就是將任務(wù)和線程兩者解耦找默,不讓兩者直接關(guān)聯(lián),才可以做后續(xù)的分配工作吼驶。線程池中是以生產(chǎn)者消費(fèi)者模式惩激,通過一個(gè)阻塞隊(duì)列來實(shí)現(xiàn)的。阻塞隊(duì)列緩存任務(wù)蟹演,工作線程從阻塞隊(duì)列中獲取任務(wù)风钻。
阻塞隊(duì)列(BlockingQueue)是一個(gè)支持兩個(gè)附加操作的隊(duì)列。這兩個(gè)附加的操作是:在隊(duì)列為空時(shí)轨帜,獲取元素的線程會等待隊(duì)列變?yōu)榉强掌枪尽.?dāng)隊(duì)列滿時(shí),存儲元素的線程會等待隊(duì)列可用蚌父。阻塞隊(duì)列常用于生產(chǎn)者和消費(fèi)者的場景哮兰,生產(chǎn)者是往隊(duì)列里添加元素的線程,消費(fèi)者是從隊(duì)列里拿元素的線程苟弛。阻塞隊(duì)列就是生產(chǎn)者存放元素的容器喝滞,而消費(fèi)者也只從容器里拿元素。
下圖中展示了線程1往阻塞隊(duì)列中添加元素膏秫,而線程2從阻塞隊(duì)列中移除元素:
圖5 阻塞隊(duì)列
使用不同的隊(duì)列可以實(shí)現(xiàn)不一樣的任務(wù)存取策略右遭。在這里,我們可以再介紹下阻塞隊(duì)列的成員:
2.3.3 任務(wù)申請
由上文的任務(wù)分配部分可知,任務(wù)的執(zhí)行有兩種可能:一種是任務(wù)直接由新創(chuàng)建的線程執(zhí)行窘哈。另一種是線程從任務(wù)隊(duì)列中獲取任務(wù)然后執(zhí)行吹榴,執(zhí)行完任務(wù)的空閑線程會再次去從隊(duì)列中申請任務(wù)再去執(zhí)行。第一種情況僅出現(xiàn)在線程初始創(chuàng)建的時(shí)候滚婉,第二種是線程獲取任務(wù)絕大多數(shù)的情況图筹。
線程需要從任務(wù)緩存模塊中不斷地取任務(wù)執(zhí)行,幫助線程從阻塞隊(duì)列中獲取任務(wù)让腹,實(shí)現(xiàn)線程管理模塊和任務(wù)管理模塊之間的通信远剩。這部分策略由getTask方法實(shí)現(xiàn),其執(zhí)行流程如下圖所示:
圖6 獲取任務(wù)流程圖
getTask這部分進(jìn)行了多次判斷骇窍,為的是控制線程的數(shù)量瓜晤,使其符合線程池的狀態(tài)。如果線程池現(xiàn)在不應(yīng)該持有那么多線程腹纳,則會返回null值痢掠。工作線程Worker會不斷接收新任務(wù)去執(zhí)行,而當(dāng)工作線程Worker接收不到任務(wù)的時(shí)候只估,就會開始被回收志群。
2.3.4 任務(wù)拒絕
任務(wù)拒絕模塊是線程池的保護(hù)部分,線程池有一個(gè)最大的容量蛔钙,當(dāng)線程池的任務(wù)緩存隊(duì)列已滿锌云,并且線程池中的線程數(shù)目達(dá)到maximumPoolSize時(shí),就需要拒絕掉該任務(wù)吁脱,采取任務(wù)拒絕策略桑涎,保護(hù)線程池。
拒絕策略是一個(gè)接口兼贡,其設(shè)計(jì)如下:
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
用戶可以通過實(shí)現(xiàn)這個(gè)接口去定制拒絕策略攻冷,也可以選擇JDK提供的四種已有拒絕策略,其特點(diǎn)如下:
2.4 Worker線程管理
2.4.1 Worker線程
線程池為了掌握線程的狀態(tài)并維護(hù)線程的生命周期遍希,設(shè)計(jì)了線程池內(nèi)的工作線程Worker等曼。我們來看一下它的部分代碼:
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
final Thread thread;//Worker持有的線程
Runnable firstTask;//初始化的任務(wù),可以為null
}
Worker這個(gè)工作線程凿蒜,實(shí)現(xiàn)了Runnable接口禁谦,并持有一個(gè)線程thread,一個(gè)初始化的任務(wù)firstTask废封。thread是在調(diào)用構(gòu)造方法時(shí)通過ThreadFactory來創(chuàng)建的線程州泊,可以用來執(zhí)行任務(wù);firstTask用它來保存?zhèn)魅氲牡谝粋€(gè)任務(wù)漂洋,這個(gè)任務(wù)可以有也可以為null遥皂。如果這個(gè)值是非空的力喷,那么線程就會在啟動(dòng)初期立即執(zhí)行這個(gè)任務(wù),也就對應(yīng)核心線程創(chuàng)建時(shí)的情況演训;如果這個(gè)值是null弟孟,那么就需要?jiǎng)?chuàng)建一個(gè)線程去執(zhí)行任務(wù)列表(workQueue)中的任務(wù),也就是非核心線程的創(chuàng)建样悟。
Worker執(zhí)行任務(wù)的模型如下圖所示:
圖7 Worker執(zhí)行任務(wù)
線程池需要管理線程的生命周期披蕉,需要在線程長時(shí)間不運(yùn)行的時(shí)候進(jìn)行回收。線程池使用一張Hash表去持有線程的引用乌奇,這樣可以通過添加引用、移除引用這樣的操作來控制線程的生命周期眯娱。這個(gè)時(shí)候重要的就是如何判斷線程是否在運(yùn)行礁苗。
?Worker是通過繼承AQS,使用AQS來實(shí)現(xiàn)獨(dú)占鎖這個(gè)功能徙缴。沒有使用可重入鎖ReentrantLock试伙,而是使用AQS,為的就是實(shí)現(xiàn)不可重入的特性去反應(yīng)線程現(xiàn)在的執(zhí)行狀態(tài)于样。
1.lock方法一旦獲取了獨(dú)占鎖疏叨,表示當(dāng)前線程正在執(zhí)行任務(wù)中。 2.如果正在執(zhí)行任務(wù)穿剖,則不應(yīng)該中斷線程蚤蔓。 3.如果該線程現(xiàn)在不是獨(dú)占鎖的狀態(tài),也就是空閑的狀態(tài)糊余,說明它沒有在處理任務(wù)秀又,這時(shí)可以對該線程進(jìn)行中斷。 4.線程池在執(zhí)行shutdown方法或tryTerminate方法時(shí)會調(diào)用interruptIdleWorkers方法來中斷空閑的線程贬芥,interruptIdleWorkers方法會使用tryLock方法來判斷線程池中的線程是否是空閑狀態(tài)吐辙;如果線程是空閑狀態(tài)則可以安全回收。
在線程回收過程中就使用到了這種特性蘸劈,回收過程如下圖所示:
圖8 線程池回收過程
2.4.2 Worker線程增加
增加線程是通過線程池中的addWorker方法昏苏,該方法的功能就是增加一個(gè)線程,該方法不考慮線程池是在哪個(gè)階段增加的該線程威沫,這個(gè)分配線程的策略是在上個(gè)步驟完成的贤惯,該步驟僅僅完成增加線程,并使它運(yùn)行壹甥,最后返回是否成功這個(gè)結(jié)果救巷。addWorker方法有兩個(gè)參數(shù):firstTask、core句柠。firstTask參數(shù)用于指定新增的線程執(zhí)行的第一個(gè)任務(wù)浦译,該參數(shù)可以為空棒假;core參數(shù)為true表示在新增線程時(shí)會判斷當(dāng)前活動(dòng)線程數(shù)是否少于corePoolSize,false表示新增線程前需要判斷當(dāng)前活動(dòng)線程數(shù)是否少于maximumPoolSize精盅,其執(zhí)行流程如下圖所示:
圖9 申請線程執(zhí)行流程圖
2.4.3 Worker線程回收
線程池中線程的銷毀依賴JVM自動(dòng)的回收帽哑,線程池做的工作是根據(jù)當(dāng)前線程池的狀態(tài)維護(hù)一定數(shù)量的線程引用,防止這部分線程被JVM回收叹俏,當(dāng)線程池決定哪些線程需要回收時(shí)妻枕,只需要將其引用消除即可。Worker被創(chuàng)建出來后粘驰,就會不斷地進(jìn)行輪詢屡谐,然后獲取任務(wù)去執(zhí)行,核心線程可以無限等待獲取任務(wù)蝌数,非核心線程要限時(shí)獲取任務(wù)愕掏。當(dāng)Worker無法獲取到任務(wù),也就是獲取的任務(wù)為空時(shí)顶伞,循環(huán)會結(jié)束饵撑,Worker會主動(dòng)消除自身在線程池內(nèi)的引用。
try {
while (task != null || (task = getTask()) != null) {
//執(zhí)行任務(wù)
}
} finally {
processWorkerExit(w, completedAbruptly);//獲取不到任務(wù)時(shí)唆貌,主動(dòng)回收自己
}
線程回收的工作是在processWorkerExit方法完成的滑潘。
圖10 線程銷毀流程
事實(shí)上,在這個(gè)方法中锨咙,將線程引用移出線程池就已經(jīng)結(jié)束了線程銷毀的部分语卤。但由于引起線程銷毀的可能性有很多,線程池還要判斷是什么引發(fā)了這次銷毀酪刀,是否要改變線程池的現(xiàn)階段狀態(tài)粱侣,是否要根據(jù)新狀態(tài),重新分配線程蓖宦。
2.4.4 Worker線程執(zhí)行任務(wù)
在Worker類中的run方法調(diào)用了runWorker方法來執(zhí)行任務(wù)齐婴,runWorker方法的執(zhí)行過程如下:
1.while循環(huán)不斷地通過getTask()方法獲取任務(wù)。 2.getTask()方法從阻塞隊(duì)列中取任務(wù)稠茂。 3.如果線程池正在停止柠偶,那么要保證當(dāng)前線程是中斷狀態(tài),否則要保證當(dāng)前線程不是中斷狀態(tài)睬关。 4.執(zhí)行任務(wù)诱担。 5.如果getTask結(jié)果為null則跳出循環(huán),執(zhí)行processWorkerExit()方法电爹,銷毀線程蔫仙。
執(zhí)行流程如下圖所示:
圖11 執(zhí)行任務(wù)流程
三、線程池在業(yè)務(wù)中的實(shí)踐
3.1 業(yè)務(wù)背景
在當(dāng)今的互聯(lián)網(wǎng)業(yè)界丐箩,為了最大程度利用CPU的多核性能摇邦,并行運(yùn)算的能力是不可或缺的恤煞。通過線程池管理線程獲取并發(fā)性是一個(gè)非常基礎(chǔ)的操作施籍,讓我們來看兩個(gè)典型的使用線程池獲取并發(fā)性的場景居扒。
場景1:快速響應(yīng)用戶請求
描述:用戶發(fā)起的實(shí)時(shí)請求,服務(wù)追求響應(yīng)時(shí)間丑慎。比如說用戶要查看一個(gè)商品的信息喜喂,那么我們需要將商品維度的一系列信息如商品的價(jià)格、優(yōu)惠竿裂、庫存玉吁、圖片等等聚合起來,展示給用戶腻异。
分析:從用戶體驗(yàn)角度看诈茧,這個(gè)結(jié)果響應(yīng)的越快越好,如果一個(gè)頁面半天都刷不出捂掰,用戶可能就放棄查看這個(gè)商品了。而面向用戶的功能聚合通常非常復(fù)雜曾沈,伴隨著調(diào)用與調(diào)用之間的級聯(lián)这嚣、多級級聯(lián)等情況,業(yè)務(wù)開發(fā)同學(xué)往往會選擇使用線程池這種簡單的方式塞俱,將調(diào)用封裝成任務(wù)并行的執(zhí)行姐帚,縮短總體響應(yīng)時(shí)間。另外障涯,使用線程池也是有考量的罐旗,這種場景最重要的就是獲取最大的響應(yīng)速度去滿足用戶,所以應(yīng)該不設(shè)置隊(duì)列去緩沖并發(fā)任務(wù)唯蝶,調(diào)高corePoolSize和maxPoolSize去盡可能創(chuàng)造多的線程快速執(zhí)行任務(wù)九秀。
圖12 并行執(zhí)行任務(wù)提升任務(wù)響應(yīng)速度
場景2:快速處理批量任務(wù)
描述:離線的大量計(jì)算任務(wù),需要快速執(zhí)行粘我。比如說鼓蜒,統(tǒng)計(jì)某個(gè)報(bào)表,需要計(jì)算出全國各個(gè)門店中有哪些商品有某種屬性征字,用于后續(xù)營銷策略的分析都弹,那么我們需要查詢?nèi)珖虚T店中的所有商品,并且記錄具有某屬性的商品匙姜,然后快速生成報(bào)表畅厢。
分析:這種場景需要執(zhí)行大量的任務(wù),我們也會希望任務(wù)執(zhí)行的越快越好氮昧。這種情況下框杜,也應(yīng)該使用多線程策略浦楣,并行計(jì)算。但與響應(yīng)速度優(yōu)先的場景區(qū)別在于霸琴,這類場景任務(wù)量巨大椒振,并不需要瞬時(shí)的完成,而是關(guān)注如何使用有限的資源梧乘,盡可能在單位時(shí)間內(nèi)處理更多的任務(wù)澎迎,也就是吞吐量優(yōu)先的問題。所以應(yīng)該設(shè)置隊(duì)列去緩沖并發(fā)任務(wù)选调,調(diào)整合適的corePoolSize去設(shè)置處理任務(wù)的線程數(shù)夹供。在這里,設(shè)置的線程數(shù)過多可能還會引發(fā)線程上下文切換頻繁的問題仁堪,也會降低處理任務(wù)的速度哮洽,降低吞吐量。
圖13 并行執(zhí)行任務(wù)提升批量任務(wù)執(zhí)行速度
3.2 實(shí)際問題及方案思考
線程池使用面臨的核心的問題在于:線程池的參數(shù)并不好配置弦聂。一方面線程池的運(yùn)行機(jī)制不是很好理解鸟辅,配置合理需要強(qiáng)依賴開發(fā)人員的個(gè)人經(jīng)驗(yàn)和知識;另一方面莺葫,線程池執(zhí)行的情況和任務(wù)類型相關(guān)性較大匪凉,IO密集型和CPU密集型的任務(wù)運(yùn)行起來的情況差異非常大,這導(dǎo)致業(yè)界并沒有一些成熟的經(jīng)驗(yàn)策略幫助開發(fā)人員參考捺檬。
關(guān)于線程池配置不合理引發(fā)的故障再层,公司內(nèi)部有較多記錄,下面舉一些例子:
Case1:2018年XX頁面展示接口大量調(diào)用降級:
事故描述:XX頁面展示接口產(chǎn)生大量調(diào)用降級堡纬,數(shù)量級在幾十到上百聂受。
事故原因:該服務(wù)展示接口內(nèi)部邏輯使用線程池做并行計(jì)算,由于沒有預(yù)估好調(diào)用的流量烤镐,導(dǎo)致最大核心數(shù)設(shè)置偏小蛋济,大量拋出RejectedExecutionException,觸發(fā)接口降級條件炮叶,示意圖如下:
圖14 線程數(shù)核心設(shè)置過小引發(fā)RejectExecutionException
Case2:2018年XX業(yè)務(wù)服務(wù)不可用S2級故障
事故描述:XX業(yè)務(wù)提供的服務(wù)執(zhí)行時(shí)間過長瘫俊,作為上游服務(wù)整體超時(shí),大量下游服務(wù)調(diào)用失敗悴灵。
事故原因:該服務(wù)處理請求內(nèi)部邏輯使用線程池做資源隔離扛芽,由于隊(duì)列設(shè)置過長,最大線程數(shù)設(shè)置失效积瞒,導(dǎo)致請求數(shù)量增加時(shí)川尖,大量任務(wù)堆積在隊(duì)列中,任務(wù)執(zhí)行時(shí)間過長茫孔,最終導(dǎo)致下游服務(wù)的大量調(diào)用超時(shí)失敗叮喳。示意圖如下:
圖15 線程池隊(duì)列長度設(shè)置過長、corePoolSize設(shè)置過小導(dǎo)致任務(wù)執(zhí)行速度低
業(yè)務(wù)中要使用線程池馍悟,而使用不當(dāng)又會導(dǎo)致故障畔濒,那么我們怎樣才能更好地使用線程池呢?針對這個(gè)問題锣咒,我們下面延展幾個(gè)方向:
1. 能否不用線程池?
回到最初的問題炫惩,業(yè)務(wù)使用線程池是為了獲取并發(fā)性猜嘱,對于獲取并發(fā)性伐债,是否可以有什么其他的方案呢替代驻民?我們嘗試進(jìn)行了一些其他方案的調(diào)研:
綜合考慮,這些新的方案都能在某種情況下提升并行任務(wù)的性能悼嫉,然而本次重點(diǎn)解決的問題是如何更簡易艇潭、更安全地獲得的并發(fā)性。另外戏蔑,Actor模型的應(yīng)用實(shí)際上甚少蹋凝,只在Scala中使用廣泛,協(xié)程框架在Java中維護(hù)的也不成熟总棵。這三者現(xiàn)階段都不是足夠的易用鳍寂,也并不能解決業(yè)務(wù)上現(xiàn)階段的問題。
2. 追求參數(shù)設(shè)置合理性彻舰?
有沒有一種計(jì)算公式,能夠讓開發(fā)同學(xué)很簡易地計(jì)算出某種場景中的線程池應(yīng)該是什么參數(shù)呢候味?
帶著這樣的疑問刃唤,我們調(diào)研了業(yè)界的一些線程池參數(shù)配置方案:
調(diào)研了以上業(yè)界方案后,我們并沒有得出通用的線程池計(jì)算方式白群。并發(fā)任務(wù)的執(zhí)行情況和任務(wù)類型相關(guān)尚胞,IO密集型和CPU密集型的任務(wù)運(yùn)行起來的情況差異非常大,但這種占比是較難合理預(yù)估的帜慢,這導(dǎo)致很難有一個(gè)簡單有效的通用公式幫我們直接計(jì)算出結(jié)果笼裳。
3. 線程池參數(shù)動(dòng)態(tài)化?
盡管經(jīng)過謹(jǐn)慎的評估粱玲,仍然不能夠保證一次計(jì)算出來合適的參數(shù)躬柬,那么我們是否可以將修改線程池參數(shù)的成本降下來,這樣至少可以發(fā)生故障的時(shí)候可以快速調(diào)整從而縮短故障恢復(fù)的時(shí)間呢抽减?基于這個(gè)思考允青,我們是否可以將線程池的參數(shù)從代碼中遷移到分布式配置中心上,實(shí)現(xiàn)線程池參數(shù)可動(dòng)態(tài)配置和即時(shí)生效卵沉,線程池參數(shù)動(dòng)態(tài)化前后的參數(shù)修改流程對比如下:
圖16 動(dòng)態(tài)修改線程池參數(shù)新舊流程對比
基于以上三個(gè)方向?qū)Ρ鹊唢保覀兛梢钥闯鰠?shù)動(dòng)態(tài)化方向簡單有效法牲。
3.3 動(dòng)態(tài)化線程池
3.3.1 整體設(shè)計(jì)
動(dòng)態(tài)化線程池的核心設(shè)計(jì)包括以下三個(gè)方面:
- 簡化線程池配置:線程池構(gòu)造參數(shù)有8個(gè),但是最核心的是3個(gè):corePoolSize琼掠、maximumPoolSize拒垃,workQueue,它們最大程度地決定了線程池的任務(wù)分配和線程分配策略瓷蛙〉课停考慮到在實(shí)際應(yīng)用中我們獲取并發(fā)性的場景主要是兩種:(1)并行執(zhí)行子任務(wù),提高響應(yīng)速度速挑。這種情況下谤牡,應(yīng)該使用同步隊(duì)列,沒有什么任務(wù)應(yīng)該被緩存下來姥宝,而是應(yīng)該立即執(zhí)行翅萤。(2)并行執(zhí)行大批次任務(wù),提升吞吐量腊满。這種情況下套么,應(yīng)該使用有界隊(duì)列,使用隊(duì)列去緩沖大批量的任務(wù)碳蛋,隊(duì)列容量必須聲明胚泌,防止任務(wù)無限制堆積。所以線程池只需要提供這三個(gè)關(guān)鍵參數(shù)的配置肃弟,并且提供兩種隊(duì)列的選擇玷室,就可以滿足絕大多數(shù)的業(yè)務(wù)需求,Less is More笤受。
- 參數(shù)可動(dòng)態(tài)修改:為了解決參數(shù)不好配穷缤,修改參數(shù)成本高等問題。在Java線程池留有高擴(kuò)展性的基礎(chǔ)上箩兽,封裝線程池津肛,允許線程池監(jiān)聽同步外部的消息,根據(jù)消息進(jìn)行修改配置汗贫。將線程池的配置放置在平臺側(cè)身坐,允許開發(fā)同學(xué)簡單的查看、修改線程池配置落包。
- 增加線程池監(jiān)控:對某事物缺乏狀態(tài)的觀測部蛇,就對其改進(jìn)無從下手。在線程池執(zhí)行任務(wù)的生命周期添加監(jiān)控能力咐蝇,幫助開發(fā)同學(xué)了解線程池狀態(tài)搪花。
圖17 動(dòng)態(tài)化線程池整體設(shè)計(jì)
3.3.2 功能架構(gòu)
動(dòng)態(tài)化線程池提供如下功能:
動(dòng)態(tài)調(diào)參:支持線程池參數(shù)動(dòng)態(tài)調(diào)整、界面化操作;包括修改線程池核心大小撮竿、最大核心大小吮便、隊(duì)列長度等;參數(shù)修改后及時(shí)生效幢踏。 任務(wù)監(jiān)控:支持應(yīng)用粒度髓需、線程池粒度、任務(wù)粒度的Transaction監(jiān)控房蝉;可以看到線程池的任務(wù)執(zhí)行情況僚匆、最大任務(wù)執(zhí)行時(shí)間、平均任務(wù)執(zhí)行時(shí)間搭幻、95/99線等咧擂。 負(fù)載告警:線程池隊(duì)列任務(wù)積壓到一定值的時(shí)候會通過大象(美團(tuán)內(nèi)部通訊工具)告知應(yīng)用開發(fā)負(fù)責(zé)人;當(dāng)線程池負(fù)載數(shù)達(dá)到一定閾值的時(shí)候會通過大象告知應(yīng)用開發(fā)負(fù)責(zé)人檀蹋。 操作監(jiān)控:創(chuàng)建/修改和刪除線程池都會通知到應(yīng)用的開發(fā)負(fù)責(zé)人松申。 操作日志:可以查看線程池參數(shù)的修改記錄,誰在什么時(shí)候修改了線程池參數(shù)俯逾、修改前的參數(shù)值是什么贸桶。 權(quán)限校驗(yàn):只有應(yīng)用開發(fā)負(fù)責(zé)人才能夠修改應(yīng)用的線程池參數(shù)。
圖18 動(dòng)態(tài)化線程池功能架構(gòu)
參數(shù)動(dòng)態(tài)化
JDK原生線程池ThreadPoolExecutor提供了如下幾個(gè)public的setter方法桌肴,如下圖所示:
圖19 JDK 線程池參數(shù)設(shè)置接口
JDK允許線程池使用方通過ThreadPoolExecutor的實(shí)例來動(dòng)態(tài)設(shè)置線程池的核心策略皇筛,以setCorePoolSize為方法例,在運(yùn)行期線程池使用方調(diào)用此方法設(shè)置corePoolSize之后坠七,線程池會直接覆蓋原來的corePoolSize值水醋,并且基于當(dāng)前值和原始值的比較結(jié)果采取不同的處理策略。對于當(dāng)前值小于當(dāng)前工作線程數(shù)的情況彪置,說明有多余的worker線程拄踪,此時(shí)會向當(dāng)前idle的worker線程發(fā)起中斷請求以實(shí)現(xiàn)回收,多余的worker在下次idel的時(shí)候也會被回收悉稠;對于當(dāng)前值大于原始值且當(dāng)前隊(duì)列中有待執(zhí)行任務(wù)宫蛆,則線程池會創(chuàng)建新的worker線程來執(zhí)行隊(duì)列任務(wù)艘包,setCorePoolSize具體流程如下:
圖20 setCorePoolSize方法執(zhí)行流程
線程池內(nèi)部會處理好當(dāng)前狀態(tài)做到平滑修改的猛,其他幾個(gè)方法限于篇幅,這里不一一介紹想虎。重點(diǎn)是基于這幾個(gè)public方法卦尊,我們只需要維護(hù)ThreadPoolExecutor的實(shí)例,并且在需要修改的時(shí)候拿到實(shí)例修改其參數(shù)即可舌厨∑袢矗基于以上的思路,我們實(shí)現(xiàn)了線程池參數(shù)的動(dòng)態(tài)化、線程池參數(shù)在管理平臺可配置可修改躏哩,其效果圖如下圖所示:
圖21 可動(dòng)態(tài)修改線程池參數(shù)
用戶可以在管理平臺上通過線程池的名字找到指定的線程池署浩,然后對其參數(shù)進(jìn)行修改,保存后會實(shí)時(shí)生效扫尺。目前支持的動(dòng)態(tài)參數(shù)包括核心數(shù)筋栋、最大值、隊(duì)列長度等正驻。除此之外弊攘,在界面中,我們還能看到用戶可以配置是否開啟告警姑曙、隊(duì)列等待任務(wù)告警閾值襟交、活躍度告警等等。關(guān)于監(jiān)控和告警伤靠,我們下面一節(jié)會對齊進(jìn)行介紹捣域。
線程池監(jiān)控
除了參數(shù)動(dòng)態(tài)化之外,為了更好地使用線程池醋界,我們需要對線程池的運(yùn)行狀況有感知竟宋,比如當(dāng)前線程池的負(fù)載是怎么樣的?分配的資源夠不夠用形纺?任務(wù)的執(zhí)行情況是怎么樣的丘侠?是長任務(wù)還是短任務(wù)?基于對這些問題的思考逐样,動(dòng)態(tài)化線程池提供了多個(gè)維度的監(jiān)控和告警能力蜗字,包括:線程池活躍度、任務(wù)的執(zhí)行Transaction(頻率脂新、耗時(shí))挪捕、Reject異常、線程池內(nèi)部統(tǒng)計(jì)信息等等争便,既能幫助用戶從多個(gè)維度分析線程池的使用情況级零,又能在出現(xiàn)問題第一時(shí)間通知到用戶,從而避免故障或加速故障恢復(fù)滞乙。
1. 負(fù)載監(jiān)控和告警
線程池負(fù)載關(guān)注的核心問題是:基于當(dāng)前線程池參數(shù)分配的資源夠不夠奏纪。對于這個(gè)問題,我們可以從事前和事中兩個(gè)角度來看斩启。事前序调,線程池定義了“活躍度”這個(gè)概念,來讓用戶在發(fā)生Reject異常之前能夠感知線程池負(fù)載問題兔簇,線程池活躍度計(jì)算公式為:線程池活躍度 = activeCount/maximumPoolSize发绢。這個(gè)公式代表當(dāng)活躍線程數(shù)趨向于maximumPoolSize的時(shí)候硬耍,代表線程負(fù)載趨高。事中边酒,也可以從兩方面來看線程池的過載判定條件经柴,一個(gè)是發(fā)生了Reject異常,一個(gè)是隊(duì)列中有等待任務(wù)(支持定制閾值)墩朦。以上兩種情況發(fā)生了都會觸發(fā)告警口锭,告警信息會通過大象推送給服務(wù)所關(guān)聯(lián)的負(fù)責(zé)人。
圖22 大象告警通知
2. 任務(wù)級精細(xì)化監(jiān)控
在傳統(tǒng)的線程池應(yīng)用場景中介杆,線程池中的任務(wù)執(zhí)行情況對于用戶來說是透明的鹃操。比如在一個(gè)具體的業(yè)務(wù)場景中,業(yè)務(wù)開發(fā)申請了一個(gè)線程池同時(shí)用于執(zhí)行兩種任務(wù)春哨,一個(gè)是發(fā)消息任務(wù)荆隘、一個(gè)是發(fā)短信任務(wù),這兩類任務(wù)實(shí)際執(zhí)行的頻率和時(shí)長對于用戶來說沒有一個(gè)直觀的感受赴背,很可能這兩類任務(wù)不適合共享一個(gè)線程池椰拒,但是由于用戶無法感知,因此也無從優(yōu)化凰荚。動(dòng)態(tài)化線程池內(nèi)部實(shí)現(xiàn)了任務(wù)級別的埋點(diǎn)燃观,且允許為不同的業(yè)務(wù)任務(wù)指定具有業(yè)務(wù)含義的名稱,線程池內(nèi)部基于這個(gè)名稱做Transaction打點(diǎn)便瑟,基于這個(gè)功能缆毁,用戶可以看到線程池內(nèi)部任務(wù)級別的執(zhí)行情況,且區(qū)分業(yè)務(wù)到涂,任務(wù)監(jiān)控示意圖如下圖所示:
圖23 線程池任務(wù)執(zhí)行監(jiān)控
3. 運(yùn)行時(shí)狀態(tài)實(shí)時(shí)查看
用戶基于JDK原生線程池ThreadPoolExecutor提供的幾個(gè)public的getter方法脊框,可以讀取到當(dāng)前線程池的運(yùn)行狀態(tài)以及參數(shù),如下圖所示:
圖24 線程池實(shí)時(shí)運(yùn)行情況
動(dòng)態(tài)化線程池基于這幾個(gè)接口封裝了運(yùn)行時(shí)狀態(tài)實(shí)時(shí)查看的功能践啄,用戶基于這個(gè)功能可以了解線程池的實(shí)時(shí)狀態(tài)浇雹,比如當(dāng)前有多少個(gè)工作線程,執(zhí)行了多少個(gè)任務(wù)屿讽,隊(duì)列中等待的任務(wù)數(shù)等等昭灵。效果如下圖所示:
圖25 線程池實(shí)時(shí)運(yùn)行情況
3.4 實(shí)踐總結(jié)
面對業(yè)務(wù)中使用線程池遇到的實(shí)際問題,我們曾回到支持并發(fā)性問題本身來思考有沒有取代線程池的方案伐谈,也曾嘗試著去追求線程池參數(shù)設(shè)置的合理性烂完,但面對業(yè)界方案具體落地的復(fù)雜性、可維護(hù)性以及真實(shí)運(yùn)行環(huán)境的不確定性衩婚,我們在前兩個(gè)方向上可謂“舉步維艱”窜护。最終效斑,我們回到線程池參數(shù)動(dòng)態(tài)化方向上探索非春,得出一個(gè)且可以解決業(yè)務(wù)問題的方案,雖然本質(zhì)上還是沒有逃離使用線程池的范疇,但是在成本和收益之間奇昙,算是取得了一個(gè)很好的平衡护侮。成本在于實(shí)現(xiàn)動(dòng)態(tài)化以及監(jiān)控成本不高,收益在于:在不顛覆原有線程池使用方式的基礎(chǔ)之上储耐,從降低線程池參數(shù)修改的成本以及多維度監(jiān)控這兩個(gè)方面降低了故障發(fā)生的概率羊初。