這篇文章分別從線程池大小參數(shù)的設(shè)置、工作線程的創(chuàng)建、空閑線程的回收、阻塞隊(duì)列的使用、任務(wù)拒絕策略、線程池Hook等方面來(lái)了解線程池的使用,其中涉及到一些細(xì)節(jié)包括不同參數(shù)、不同隊(duì)列、不同拒絕策略的選擇、產(chǎn)生的影響和行為、為更好的使用線程池奠定知識(shí)基礎(chǔ),其中值得注意的部分我用粗體標(biāo)識(shí)。 ExecutorService基于池化的線程來(lái)執(zhí)行用戶提交的任務(wù),通??梢院?jiǎn)單的通過(guò)Executors提供的工廠方法來(lái)創(chuàng)建ThreadPoolExecutor實(shí)例。 線程池解決的兩個(gè)問(wèn)題:1)線程池通過(guò)減少每次做任務(wù)的時(shí)候產(chǎn)生的性能消耗來(lái)優(yōu)化執(zhí)行大量的異步任務(wù)的時(shí)候的系統(tǒng)性能。2)線程池還提供了限制和管理批量任務(wù)被執(zhí)行的時(shí)候消耗的資源、線程的方法。另外ThreadPoolExecutor還提供了簡(jiǎn)單的統(tǒng)計(jì)功能,比如當(dāng)前有多少任務(wù)被執(zhí)行完了。 快速開(kāi)始 為了使得線程池適合大量不同的應(yīng)用上下文環(huán)境,ThreadPoolExecutor提供了很多可以配置的參數(shù)和可被用來(lái)擴(kuò)展的鉤子。然而,用戶還可以通過(guò)使用Executors提供的一些工廠方法來(lái)快速創(chuàng)建ThreadPoolExecutor實(shí)例。比如:
如果上面的方法創(chuàng)建的實(shí)例不能滿足我們的需求,我們可以自己通過(guò)參數(shù)來(lái)配置,實(shí)例化一個(gè)實(shí)例。 關(guān)于線程數(shù)大小參數(shù)設(shè)置需要知道的 ThreadPoolExecutor會(huì)根據(jù)corePoolSize和maximumPoolSize來(lái)動(dòng)態(tài)調(diào)整線程池的大小:poolSize。 當(dāng)任務(wù)通過(guò)executor提交給線程池的時(shí)候,我們需要知道下面幾個(gè)點(diǎn):
核心線程WarmUp 默認(rèn)情況下,核心工作線程值在初始的時(shí)候被創(chuàng)建,當(dāng)新任務(wù)來(lái)到的時(shí)候被啟動(dòng),但是我們可以通過(guò)重寫(xiě)prestartCoreThread或prestartCoreThreads方法來(lái)改變這種行為。通常場(chǎng)景我們可以在應(yīng)用啟動(dòng)的時(shí)候來(lái)WarmUp核心線程,從而達(dá)到任務(wù)過(guò)來(lái)能夠立馬執(zhí)行的結(jié)果,使得初始任務(wù)處理的時(shí)間得到一定優(yōu)化。 定制工作線程的創(chuàng)建 新的線程是通過(guò)ThreadFactory來(lái)創(chuàng)建的,如果沒(méi)有指定,默認(rèn)的Executors#defaultThreadFactory將被使用,這個(gè)時(shí)候創(chuàng)建的線程將都屬于同一個(gè)線程組,擁有同樣的優(yōu)先級(jí)和daemon狀態(tài)。擴(kuò)展配置ThreadFactory,我們可以配置線程的名字、線程組合daemon狀態(tài)。如果調(diào)用ThreadFactory#createThread的時(shí)候失敗,將返回null,executor將不會(huì)執(zhí)行任何任務(wù)。 空閑線程回收 如果當(dāng)前池子中的工作線程數(shù)大于corePoolSize,如果超過(guò)這個(gè)數(shù)字的線程處于空閑的時(shí)間大于keepAliveTime,則這些線程將會(huì)被終止,這是一種減少不必要資源消耗的策略。這個(gè)參數(shù)可以在運(yùn)行時(shí)被改變,我們同樣可以將這種策略應(yīng)用給核心線程,我們可以通過(guò)調(diào)用allowCoreThreadTimeout來(lái)實(shí)現(xiàn)。 選擇合適的阻塞隊(duì)列 所有的阻塞隊(duì)列都可以被用來(lái)存放任務(wù),但是使用不同的隊(duì)列針對(duì)corePoolSize會(huì)表現(xiàn)不同的行為: 當(dāng)池中工作線程數(shù)小于corePoolSize的時(shí)候,每次來(lái)任務(wù)的時(shí)候都會(huì)創(chuàng)建一個(gè)新的工作線程。 當(dāng)池中工作線程數(shù)大于等于corePoolSize的時(shí)候,每次任務(wù)來(lái)的時(shí)候都會(huì)首先嘗試將線程放入隊(duì)列,而不是直接去創(chuàng)建線程。 如果放入隊(duì)列失敗,且當(dāng)先池中線程數(shù)小于maximumPoolSize的時(shí)候,則會(huì)創(chuàng)建一個(gè)工作線程。 下面主要是不同隊(duì)列策略表現(xiàn): 直接遞交:一種比較好的默認(rèn)選擇是使用SynchronousQueue,這種策略會(huì)將提交的任務(wù)直接傳送給工作線程,而不持有。如果當(dāng)前沒(méi)有工作線程來(lái)處理,即任務(wù)放入隊(duì)列失敗,則根據(jù)線程池的實(shí)現(xiàn),會(huì)引發(fā)新的工作線程創(chuàng)建,因此新提交的任務(wù)會(huì)被處理。這種策略在當(dāng)提交的一批任務(wù)之間有依賴關(guān)系的時(shí)候避免了鎖競(jìng)爭(zhēng)消耗。值得一提的是,這種策略最好是配合unbounded線程數(shù)來(lái)使用,從而避免任務(wù)被拒絕。同時(shí)我們必須要考慮到一種場(chǎng)景,當(dāng)任務(wù)到來(lái)的速度大于任務(wù)處理的速度,將會(huì)引起無(wú)限制的線程數(shù)不斷的增加。 無(wú)界隊(duì)列:使用無(wú)界隊(duì)列如LinkedBlockingQueue沒(méi)有指定最大容量的時(shí)候,將會(huì)引起當(dāng)核心線程都在忙的時(shí)候,新的任務(wù)被放在隊(duì)列上,因此,永遠(yuǎn)不會(huì)有大于corePoolSize的線程被創(chuàng)建,因此maximumPoolSize參數(shù)將失效。這種策略比較適合所有的任務(wù)都不相互依賴,獨(dú)立執(zhí)行。舉個(gè)例子,如網(wǎng)頁(yè)服務(wù)器中,每個(gè)線程獨(dú)立處理請(qǐng)求。但是當(dāng)任務(wù)處理速度小于任務(wù)進(jìn)入速度的時(shí)候會(huì)引起隊(duì)列的無(wú)限膨脹。 有界隊(duì)列:有界隊(duì)列如ArrayBlockingQueue幫助限制資源的消耗,但是不容易控制。隊(duì)列長(zhǎng)度和maximumPoolSize這兩個(gè)值會(huì)相互影響,使用大的隊(duì)列和小maximumPoolSize會(huì)減少CPU的使用、操作系統(tǒng)資源、上下文切換的消耗,但是會(huì)降低吞吐量,如果任務(wù)被頻繁的阻塞如IO線程,系統(tǒng)其實(shí)可以調(diào)度更多的線程。使用小的隊(duì)列通常需要大maximumPoolSize,從而使得CPU更忙一些,但是又會(huì)增加降低吞吐量的線程調(diào)度的消耗??偨Y(jié)一下是IO密集型可以考慮多些線程來(lái)平衡CPU的使用,CPU密集型可以考慮少些線程減少線程調(diào)度的消耗。 選擇適合的拒絕策略 當(dāng)新的任務(wù)到來(lái)的而線程池被關(guān)閉的時(shí)候,或線程數(shù)和隊(duì)列已經(jīng)達(dá)到上限的時(shí)候,我們需要去做一個(gè)決定,怎么拒絕這些任務(wù)。下面介紹一下常用的策略: ThreadPoolExecutor#AbortPolicy:這個(gè)策略直接拋出RejectedExecutionException異常。 ThreadPoolExecutor#CallerRunsPolicy:這個(gè)策略將會(huì)使用Caller線程來(lái)執(zhí)行這個(gè)任務(wù),這是一種feedback策略,可以降低任務(wù)提交的速度。 ThreadPoolExecutor#DiscardPolicy:這個(gè)策略將會(huì)直接丟棄任務(wù)。 ThreadPoolExecutor#DiscardOldestPolicy:這個(gè)策略將會(huì)把任務(wù)隊(duì)列頭部的任務(wù)丟棄,然后重新嘗試執(zhí)行,如果還是失敗則繼續(xù)實(shí)施策略。 除了上面的幾種策略,我們也可以通過(guò)實(shí)現(xiàn)RejectedExecutionHandler來(lái)實(shí)現(xiàn)自己的策略。 利用Hook嵌入你的行為 ThreadPoolExecutor提供了protected類型可以被覆蓋的鉤子方法,允許用戶在任務(wù)執(zhí)行之前會(huì)執(zhí)行之后做一些事情。我們可以通過(guò)它來(lái)實(shí)現(xiàn)比如初始化ThreadLocal、收集統(tǒng)計(jì)信息、如記錄日志等操作。這類Hook如beforeExecute和afterExecute。另外還有一個(gè)Hook可以用來(lái)在任務(wù)被執(zhí)行完的時(shí)候讓用戶插入邏輯,如rerminated。 如果hook方法執(zhí)行失敗,則內(nèi)部的工作線程的執(zhí)行將會(huì)失敗或被中斷。 可訪問(wèn)的隊(duì)列 getQueue方法可以用來(lái)訪問(wèn)queue隊(duì)列以進(jìn)行一些統(tǒng)計(jì)或者debug工作,我們不建議用作其他用途。同時(shí)remove方法和purge方法可以用來(lái)將任務(wù)從隊(duì)列中移除。 關(guān)閉線程池 當(dāng)線程池不在被引用并且工作線程數(shù)為0的時(shí)候,線程池將被終止。我們也可以調(diào)用shutdown來(lái)手動(dòng)終止線程池。如果我們忘記調(diào)用shutdown,為了讓線程資源被釋放,我們還可以使用keepAliveTime和allowCoreThreadTimeOut來(lái)達(dá)到目的。 寫(xiě)在最后 JAVA本身提供的API已經(jīng)可以讓我們快速的進(jìn)行基于線程池的多線程開(kāi)發(fā),但是我們必須要為我們寫(xiě)的代碼負(fù)責(zé),每一個(gè)參數(shù)的設(shè)置和策略的選擇跟不同應(yīng)用場(chǎng)景有絕對(duì)的關(guān)系。然而對(duì)于不同參數(shù)和不同策略的選擇并不是一件容易的事情,我們必須要先回答一些基礎(chǔ)問(wèn)題:每創(chuàng)建一個(gè)線程,操作系統(tǒng)為我們做了哪些事情,這個(gè)線程的操作系統(tǒng)資源消耗主要在哪部分?假如我的應(yīng)用場(chǎng)景是IO密集型的,那么我需要更多的線程還是更少的線程?假如我們的CPU操作和IO操作大概各占一半的話我們又需要如何選擇?等等一些列問(wèn)題。我認(rèn)為、多線程開(kāi)發(fā)是一件很容易的事情也是一件很不容易的事情。 更多JAVA開(kāi)發(fā)/Nginx/dubbo/SQL/ActiveMQ/高并發(fā)/架構(gòu)/分布式/性能優(yōu)化JAVA交流免費(fèi)視頻群:578455330 |
|
來(lái)自: 然并卵書(shū)屋 > 《待分類1》