java并發(fā)與多線程(五):線程池

1、線程池的好處

線程使應(yīng)用能夠更加充分合理地協(xié)調(diào)利用CPU、內(nèi)存、網(wǎng)絡(luò)、I/O等系統(tǒng)資源。線程的創(chuàng)建需要開辟虛擬機棧、本地方法棧、程序計數(shù)器等線程私有的內(nèi)存空間。在線程銷毀時需要回收這些系統(tǒng)資源。頻繁地創(chuàng)建和銷毀線程會浪費大量的系統(tǒng)資源,增加并發(fā)編程風險。另外,在服務(wù)器負載過大的時候,如何讓新的線程等待或者友好地拒絕服務(wù)?這些都是線程自身無法解決的。所以需要通過線程池協(xié)調(diào)多個線程,并實現(xiàn)類似主次線程隔離、定時執(zhí)行、周期執(zhí)行等任務(wù)。線程池的作用包括:

(1)利用線程池管理并復(fù)用線程、控制最大并發(fā)數(shù)等。
(2)實現(xiàn)任務(wù)線程隊列緩存策略和拒絕機制。
(3)實現(xiàn)某些與時間相關(guān)的功能,如定時執(zhí)行、周期執(zhí)行等。
(4)隔離線程環(huán)境。比如,交易服務(wù)和搜索服務(wù)在同一臺服務(wù)器上,分別開啟兩個線程池,交易線程的資源消耗明顯要大;因此,通過配置獨立的線程池,將較慢的交易服務(wù)與搜 索服務(wù) 隔離開,避免各服務(wù)線程消耗影響。

在連接線程池的基本作用后,我們學習一下線程池是如何創(chuàng)建線程的。首先從ThreadPoolExecutor構(gòu)造方法講起,學習如何自定義ThreadFactory和RejectedExecutionHandler,并編寫一個最簡單的線程池示例。然后,通過分析ThreadPoolExecutor的execute和addWorker兩個核心方法,學習如何把任務(wù)線程加入到線程池中運行。ThreadPoolExecutor的構(gòu)造方法如下:


image.png

image.png

第1個參數(shù):corePoolSize表示常駐核心線程數(shù)。如果等于0,則任務(wù)執(zhí)行完之后,沒有任何請求進入時銷毀線程池的線程;如果大于0,即使本地任務(wù)執(zhí)行完畢,核心線程也不會被銷毀。這個值的設(shè)置非常關(guān)鍵,設(shè)置過大會浪費資源,設(shè)置過小會導(dǎo)致線程頻繁地創(chuàng)建或銷毀。

第2個參數(shù):maximumPoolSize表示線程池能夠容納同時執(zhí)行的最大線程數(shù)。從上方實例代碼中的第1處來看,必須大于或等于1。如果待執(zhí)行的線程數(shù)大于此值,需要借助第5個參數(shù)的幫助,緩存在隊列中。如果maximumPoolSize與corePoolSize相等,即是固定大小線程池。

第3個參數(shù):keepAliveTime表示線程池中的線程空間時間,當空閑時間達到keepAliveTime值時,線程就會被銷毀,直到只剩下corePoolSize個線程為止,避免浪費內(nèi)存和句柄資源。在默認情況下,當線程池的線程數(shù)大于corePoolSize時,keepAliveTime才會起作用。但是當ThreadPoolExecutor的allowCoreThreadTimeOut變量設(shè)置為true時,核心線程超時后也會被收回。

第4個參數(shù):TimeUnit表示時間單位。keepAliveTime的時間單位通常是TimeUnit.SECONDS。

第5個參數(shù):workQueue表示緩存隊列。當請求的線程數(shù)大于maximumPoolSize時,線程機內(nèi)BlockingQueue的阻塞隊列。后續(xù)示例代碼中使用的LinkedBlockingQueue是單向鏈表,使用鎖來控制入隊和出隊的原子性,兩個鎖分別控制元素的添加和獲取,是一個生產(chǎn)消費模型隊列。

第6個參數(shù):threadFactory表示線程工廠。它用來生產(chǎn)一組相同任務(wù)的線程。線程池的命名是通過給這個factory增加組名前綴來實現(xiàn)的。在虛擬機棧分析時,就可以知道線程任務(wù)是由哪個線程工廠產(chǎn)生的。

第7個參數(shù):handler表示執(zhí)行拒絕策略的對象。當超過第5個參數(shù)workQueue的任務(wù)緩存區(qū)上限的時候,就可以通過該策略處理請求,這是一種簡單的限流保護。像某年雙十一沒有處理好訪問流量過載時的拒絕策略,導(dǎo)致內(nèi)部測試頁面被展示出來,使用戶手足無措。友好的拒絕策略可以是如下三種:
(1)保存到數(shù)據(jù)庫進行削峰填谷。在空閑時再提取出來執(zhí)行。
(2)轉(zhuǎn)向某個提示頁面。
(3)打印日志。
從代碼第2處來看,隊列、線程工廠、拒絕處理服務(wù)都必須有實例對象,但在實際編程中,很少有程序員對這三者進行實例化,而通過Executors這個線程池靜態(tài)工廠提供默認實現(xiàn),那么Executors與ThreadPoolExecutor是什么關(guān)系呢?線程池相關(guān)類圖如圖所示:


image.png

image.png

ExecutorService接口繼承了Executor接口,定義了管理線程任務(wù)的方法。ExecutorService的抽象類AbstractExecutorService提供了submi()、invokeAll()等部分方法的實現(xiàn),但是核心方法Executor.executor()并沒有在這里實現(xiàn)。因為所有的任務(wù)都在這個方法里執(zhí)行,不同實現(xiàn)會帶來不同的執(zhí)行策略,這一點在后續(xù)的ThreadPoolExecutor解析時,會一步步地分析。通過Executors的靜態(tài)工廠方法可以創(chuàng)建三個線程池的包裝對象:ForkJoinPool、ThreadPoolExecutor、ScheduledThreadPoolExecutor。Executors核心的方法有五個:

(1)Executors.new.WorkStealingPool: JDK8引入,創(chuàng)建持有足夠線程的線程池 支持給定的并行度,并通過使用多個隊列減少競爭,此構(gòu)造方法中把CPU數(shù) 量設(shè)置為默認的并行度:


image.png

(2)Executors.newCachedThreadPool: maximumPoolSize最大可以至 Integer.MAX_VALUE,是高度可伸縮的線程池,如果達到這個上限,相信沒有 任何服務(wù)器能夠繼續(xù)工作,肯定會拋出OOM異常。keepAliveTime默認為60 秒,工作線程處于空閑狀態(tài),則收回工作線程。如果任務(wù)數(shù)增加,再次創(chuàng)建出 新線程處理任務(wù)。

(3)Executors.newScheduledThreadPool: 線程數(shù)最大至Integer.MAX_VALUE, 與上述相同,存在OOM風險。它是ScheduledExecutorService接口家族的實現(xiàn) 類,支持定時及周期性任務(wù)執(zhí)行。相比Timer, ScheduledExecutorService更安 全。功能更強大,與newCacheThreadPool的區(qū)別是不回收工作線程。

(4)Executors.newSingleThreadExecutor: 創(chuàng)建一個單線程的線程池,相當于 線程串行執(zhí)行所有任務(wù),保證按任務(wù)的提交順序依次執(zhí)行。

(5)Executors.newFixedThreadPool: 輸入的參數(shù)即是固定線程數(shù),既是核心線 程數(shù)也是最大線程數(shù),不存在空閑線程,所以keepAliveTime等于0:


image.png

這里,輸入的隊列沒有指明長度,下面介紹LinkedBlockingQueue的構(gòu)造方法:


image.png

使用這樣的無界隊列,如果瞬間請求非常大,會有OOM的風險。除newWorkStealingPool外,其他四個創(chuàng)建方式都存在資源耗盡的風險。

Executors中默認的線程工廠和拒絕策略過于簡單,通常對用戶不夠友好。線程工廠需要做創(chuàng)建前的準備工作,對線程池創(chuàng)建的線程必須明確標識,就像藥品的生產(chǎn)批號一樣,為線程本身指定有意義的名稱和相應(yīng)的序列號。拒絕策略應(yīng)該考慮到業(yè)務(wù)場景,返回相應(yīng)的提示或者友好地跳轉(zhuǎn)。以下為簡單的ThreadFactory示例:


image.png

上述示例包括線程工廠和任務(wù)執(zhí)行體的定義,通過newThread方法快速、統(tǒng)一地創(chuàng)建線程任務(wù),強調(diào)線程一定要有特定意義的名稱,方便出錯時回溯。

如圖所示為排查底層公共緩存調(diào)用出錯時的截圖,綠色框采用自定義的線程工廠,明顯比藍色框默認的線程名稱擁有更多的額外信息:如調(diào)用來源、線程的業(yè)務(wù)含義,有助于快速定位到死鎖、StackOverflowError等問題。


image.png

下面再簡單地實現(xiàn)一下RejectdExecutionHandler,實現(xiàn)了接口的rejectedExecution方法,打印出當前線程池狀態(tài),源碼如下:


image.png

在ThreadPoolExecutor中提供了四個公開的內(nèi)部靜態(tài)類:
(1)AbortPolicy(默認):丟棄任務(wù)拋出RejectedExecutionException異常。
(2)DiscardPolicy:丟棄任務(wù)但是不拋出異常,這是不推薦的做法。
(3)DiscardOldestPolicy:拋棄隊列中等待最久的任務(wù),然后把當前任務(wù)加入隊列中。
(4)CallerRunsPolicy:調(diào)用任務(wù)的run()方法繞過其線程池直接執(zhí)行。
根據(jù)之前實現(xiàn)的線程工廠和拒絕策略,線程池的相關(guān)代碼實現(xiàn)如下:


image.png

image.png

image.png

當任務(wù)被拒絕的時候,拒絕策略會打印出當前線程池的大小已經(jīng)達到了maximumPoolSize=2,且隊列已滿,完成的任務(wù)提示已經(jīng)有1個(最后一行)。

2、線程池源碼詳解

在ThreadPoolExecutor的屬性定義中頻繁地用位移運算來表示線程池狀態(tài),位移運算是改變當前值的一種高效手段,包括左移與右移,下面從屬性定義開始閱讀ThreadPoolExecutor的源碼:


image.png

image.png

第1處說明,線程池的狀態(tài)用高3位表示,其中包括了符號位。五中狀態(tài)的十進制值按從小到大依次排序為:RUNNING < SHUTDOWN < STOP < TIDYING < TERMINATED,這樣設(shè)計的好吃是可以通過比較值的大小來確定線程池的狀態(tài)。例如程序中經(jīng)常會出現(xiàn)isRunning的判斷:


image.png

我們都知道Executor接口有且只有一個方法execute,通過參數(shù)傳入待執(zhí)行線程的對象。下面分析ThreadPoolExecutor關(guān)于execute方法的實現(xiàn):


image.png

第1處:execute方法在不同的階段有三次addWorker的嘗試動作。
第2處:發(fā)生拒絕的理由有兩個:(1)線程池為非RUNNING狀態(tài)(2)等待隊列已滿。
下面繼續(xù)分析addWorker方法的源碼:


image.png

image.png

image.png

這段代碼晦澀難懂,部分地方甚至違反了代碼規(guī)約,但其中蘊含的豐富的編碼知識點值得我們?nèi)W習,下面按序號來依次講解。

第1處,配合循環(huán)語句出現(xiàn)的label,類似于goto作用。Label定義時,表虛把標簽和冒號的組合語句緊緊相鄰定義在循環(huán)體之前,否則會編譯出錯。目的是在實現(xiàn)多重循環(huán)時能夠快速退出到任何一層。這種做法的出發(fā)點似乎非常貼心,但是在大型軟件項目中,濫用標簽跳轉(zhuǎn)的后果將是災(zāi)難性的。示例代碼中,在retry下方有兩個無線循環(huán),在workerCount加1成功后,直接退出兩層循環(huán)。

第2處,這樣的表達式不利于代碼閱讀,應(yīng)該改成:


image.png

第3處,與第1處的標簽呼應(yīng),AtomicInteger對象的加1操作時原子性的。Break retry表示直接跳出與retry相鄰的這個循環(huán)體。

第4處,此continue跳轉(zhuǎn)至標簽處,繼續(xù)執(zhí)行循環(huán)。如果條件為假,則說明線程池還處于運行狀態(tài),即繼續(xù)在for(;;)循環(huán)內(nèi)執(zhí)行。

第5處,compareAndIncrementWorkerCount方法執(zhí)行失敗的概率非常低。即使失敗,再次執(zhí)行時成功的概率也是極高的,類似于自旋鎖原理。這里的處理邏輯是先加1,創(chuàng)建失敗在減1,這是輕量處理并發(fā)線程的方式。如果先創(chuàng)建線程,成功再加1,當發(fā)現(xiàn)超出限制后再銷毀線程,那么這樣的處理方式明顯比前者代價要大。

第6處,Worker對象是工作線程的核心類實現(xiàn),部分源碼如下:


image.png

image.png

線程池的相關(guān)源碼比較精煉,還包括線程池的銷毀、任務(wù)提取和消費等,與線程狀態(tài)圖一樣,線程池也有自己獨立的狀態(tài)轉(zhuǎn)化流程,本節(jié)不再展開。總結(jié)一下,使用線程池要注意如下幾點:
(1)合理設(shè)置各類參數(shù),應(yīng)根據(jù)實際業(yè)務(wù)場景來設(shè)置合理的工作線程數(shù)。
(2)線程資源必須通過線程池提供,不允許在應(yīng)用中自行顯式創(chuàng)建線程。
(3)創(chuàng)建線程或線程池時請指定有意義的線程名稱,方便出錯時回溯。
線程池不允許使用Executors,而是通過ThreadPoolExecutor的方式創(chuàng)建,這樣的處理方式能更加明確線程池的運行規(guī)則,規(guī)避資源耗盡的風險。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容