如何根據(jù)系統(tǒng)的業(yè)務(wù)場景需求定制自己的線程池?
? ?【本文由于內(nèi)容相對比較嚴(yán)肅,因此基本上沒有怎么大白話,這是因為筆者在寫作的時候已經(jīng)厭倦了所謂的大白話寫作。希望讀者理解】
? ? 前面的章節(jié)中我們已經(jīng)對線程池的作用、優(yōu)勢、原理都做了細(xì)致的學(xué)習(xí)和了解。但還是不免有疑問,線程池有那么多的參數(shù)和類型,在實際的開發(fā)中,我們應(yīng)該如何設(shè)置這些參數(shù)呢?是直接使用Executors提供的線程池實現(xiàn)還是自定義線程池?這都是我們本節(jié)要回答的問題,那么就請跟隨筆者一起來研究一下在實戰(zhàn)中如何根據(jù)系統(tǒng)的業(yè)務(wù)場景需求來定制自己的線程池吧。
??? 一般情況下,其實Executors提供的幾種實現(xiàn)已經(jīng)足夠我們使用了,比如:newCachedThreadPool()、newFixedThreadPool()以及newSingleThreadExecutor()。
??? 如圖1流程圖所示,我將不同的業(yè)務(wù)場景適合的線程池類型畫了出來。

圖1
??? 如果在業(yè)務(wù)場景中使用一個線程就足夠了,那么直接選擇擁有一個核心工作線程的newSingleThreadExecutor()就能滿足要求;
??? 如果一個線程不夠,但是能夠判斷線程數(shù)量是有限的,那么只需要指定工作線程數(shù)量N,通過newFixedThreadPool(N)就能夠滿足要求;
??? 如果需要通過創(chuàng)建線程來應(yīng)對一定程度的突發(fā)流量,保證任務(wù)處理的即時性,那么使用newCachedThreadPool()也是比較合理的。需要注意的是,如果突發(fā)流量很大,比如每秒上萬的突增流量,那么使用newCachedThreadPool()就需要慎重,因為會導(dǎo)致出現(xiàn)java.lang.OutOfMemoryError異常。
??? 需要注意的是,我們這里提到的newSingleThreadExecutor()以及newFixedThreadPool(N)線程池,使用的都是LinkedBlockingQueue無界隊列。如果業(yè)務(wù)場景不適合使用無界隊列,比如:任務(wù)攜帶的數(shù)據(jù)過多,且任務(wù)并發(fā)量大,那么使用基于LinkedBlockingQueue無界隊列的線程池就需要慎重。
??? 也就是說,Executors提供的默認(rèn)線程池也是特定場景下才適用,并不是萬能藥。
??? 那么問題來了,生產(chǎn)中應(yīng)當(dāng)如何使用線程池才比較合理呢?答案很簡單,就是自定義線程池。
??? 自定義線程池,其實就是根據(jù)自己業(yè)務(wù)場景,使用不同的參數(shù)去對線程池進行定制,從而滿足具體的業(yè)務(wù)場景。
??? 定制線程池的要點,其實就是ThreadPoolExecutor的核心構(gòu)造參數(shù)的指定,主要在于指定合理的核心線程數(shù)、最大線程數(shù)、選擇合適的工作隊列、自定義線程工廠以及選擇合適的拒絕策略。我們針對每個參數(shù)具體討論一下。
指定線程數(shù)量?
??? 線程數(shù)量并沒有一個標(biāo)準(zhǔn)答案,它主要依賴機器的CPU個數(shù)以及JVM虛擬機堆的大小,一般情況下,CPU個數(shù)是更加主要的影響因素。
??? 實際生產(chǎn)中,我們根據(jù)任務(wù)關(guān)注點的不同,將任務(wù)劃分為:CPU密集型(或者叫計算密集型)、I/O密集型兩大類,如圖2所示。也有一種叫做混合類型的任務(wù),也就是既包含計算又包含I/O操作,不是我們討論的重點因此不單獨討論。感興趣的兄弟可以自己去單獨了解。

圖2
??? 一般來說,對于CPU密集型的任務(wù),由于CPU計算速度很快,任務(wù)在短時間內(nèi)就能夠通過CPU超強的計算能力執(zhí)行完成,因此我們可以設(shè)置核心線程數(shù)corePoolSize為N(CPU個數(shù))+1,之所以要設(shè)置為CPU個數(shù)加1,主要原因在于為了防止某些情況下出現(xiàn)等待情況導(dǎo)致沒有線程可用,比如說發(fā)生了缺頁中斷時,就會出現(xiàn)等待的情況。因此設(shè)置一個額外的線程,可以保證繼續(xù)使用CPU時間片。
??? 而對于I/O密集型的任務(wù),我們可以為最大線程數(shù)多設(shè)置一些線程。原因在于相比CPU密集型任務(wù),I/O密集型任務(wù)在執(zhí)行過程中由于等待I/O結(jié)果花費的時間要明顯大于CPU計算所花費的時間,而且我們都知道,處于I/O等待狀態(tài)的線程并不會消耗CPU資源,因此可以多設(shè)置一些線程。一般情況下,我們將其設(shè)置為CPU個數(shù)的倍數(shù),常見的玩兒法是設(shè)置為N(CPU個數(shù))*2。
??? 對于I/O密集型任務(wù),我們還要注意核心線程數(shù)不用設(shè)置的很大,原因在于I/O操作本身會導(dǎo)致上下文切換的發(fā)生,尤其是阻塞式I/O。因此建議將I/O密集型的核心線程數(shù)corePoolSize限制為1,最大線程數(shù)maximumPoolSize設(shè)置為N(CPU個數(shù))*2。當(dāng)線程池中只要一個線程的時候,能夠很從容的應(yīng)對提交的任務(wù),此時的上下文切換相當(dāng)少。然后隨著任務(wù)逐漸增加,再慢慢的增加線程數(shù)量至最大線程數(shù)。這樣做既不浪費資源,還很靈活的支持了任務(wù)增加的場景。
??? 需要注意的是我們這里給出的是一種理論的參考配置,實際開發(fā)中,由于對性能的要求以及機器配置的不同,我們不能太過于死板教條的照搬文中的配置,還是需要根據(jù)具體的情況進行合適的調(diào)整,比如考慮CPU的利用率,任務(wù)執(zhí)行過程中的等待時間等。但是一般來說,使用我們提到的配置是一種比較穩(wěn)妥合適的方式。關(guān)于如何計算合理的線程池大小其實是有一個公式的,這里貼一下公式的內(nèi)容,公式背后的深層次的原理就留給大家去探索學(xué)習(xí),公式內(nèi)容如圖3所示。

圖3
??? 在Java中,通過Runtime.getRuntime().availableProcessors()就可以很方便的獲取到JVM所在機器的CPU個數(shù),從而方便我們指定具體的線程個數(shù)。
選擇合適的工作隊列
?? 我們接著來看一下如何選擇合適的工作隊列。
??? 工作隊列通常有無界隊列、有界隊列、同步隊列三種類型。每個隊列都有它自己的特點和用途,按照慣例還是用一張圖來說明,如圖4所示。

圖4
??? 在圖4中,我們將每種工作隊列的特點,代表的實現(xiàn),以及使用的注意點都做了詳細(xì)說明,大家可以認(rèn)真閱讀圖中的內(nèi)容,我們就不再贅述了。
自定義線程工廠
??? 一般來說,我們還需要定義線程工廠,給自定義的線程池起一個個性化的名字,這有助于我們在查找日志的時候精確的定位到具體的某個線程池。
??? 自定義線程工廠,需要實現(xiàn)ThreadFactory接口,此處提供一個參考實現(xiàn),如圖5所示,大家可以根據(jù)這個代碼樣例進行擴展,實現(xiàn)自己的線程工廠,當(dāng)然也建議大家多去閱讀優(yōu)秀的開源框架,比如Netty、Tomcat,它們都提供了優(yōu)秀的自定義的線程工廠的實現(xiàn)。

圖5
選擇合適的拒絕策略
??? 我們接著聊聊如何選擇合適的拒絕策略,關(guān)于ThreadPoolExecutor默認(rèn)的拒絕策略及其特點,我們可以參考圖6。

圖6
??? 一般來說,直接使用AbortPolicy拋出異常即可,但是如果說我們要求即便觸發(fā)了拒絕策略,任務(wù)也得執(zhí)行完成不能丟棄,那么選擇CallerRunsPolicy拒絕策略即可。如果說這幾種拒絕策略都滿足不了我們的需求的話,就可以自定義拒絕策略,只需要實現(xiàn)RejectedExecutionHandler接口即可實現(xiàn)自定義的拒絕策略。
自定義線程池代碼案例
?? 上文中,我們對自定義線程池中需要注意的要點都進行了詳細(xì)的圖文并茂的講解,相信你已經(jīng)有所收獲。這個部分我們就趁熱打鐵,編寫一個完整的自定義線程池的案例。
??? 我們以I/O密集型任務(wù)為例,實現(xiàn)一個自定義線程池的案例,具體代碼如圖7所示。

圖7
??? 我們定義了一個訂單同步線程池,指定核心線程數(shù)為CPU數(shù)量+1,最大線程數(shù)為CPU數(shù)量*2,并指定非核心線程數(shù)的存活時間為60s。
??? 我們使用了有界的LinkedBlockingQueue作為工作隊列,指定大小為500,這個參數(shù)可以根據(jù)實際情況自定義,比如通過配置文件動態(tài)指定。
??? 同時還定義了自定義的線程工廠,為線程池設(shè)置了名稱“sync-order-info-thread-pool”,便于方便查詢?nèi)罩?。最后指定了拒絕策略為CallerRunsPolicy(),保證只要JVM進程正常運行,任務(wù)一定能夠被執(zhí)行到。
??? 我們只需要編寫一個方法,將該自定義的線程池的引用返回,就可以讓業(yè)務(wù)邏輯在需要的場景隨時使用該自定義線程池了。實際開發(fā)中,我們更多的會用到Spring框架進行代碼編寫,我們只需要定義一個ThreadPoolExecutor的bean,即可在需要使用的地方進行注入,進而使用其進行異步任務(wù)提交等操作了,如圖8所示。

圖8
??? 本節(jié),我們重點討論了如何根據(jù)系統(tǒng)的實際業(yè)務(wù)是需求自定義線程池,在接下來的文章中,我們將通過線程池來實現(xiàn)互聯(lián)網(wǎng)場景下的驗證碼保護服務(wù),敬請期待。
