「Java 路線」| 線程池

點(diǎn)贊關(guān)注,不再迷路,你的支持對(duì)我意義重大!

?? Hi,我是丑丑。本文 「Java 路線」| 導(dǎo)讀 —— 他山之石,可以攻玉 已收錄,這里有 Android 進(jìn)階成長路線筆記 & 博客,歡迎跟著彭丑丑一起成長。(聯(lián)系方式在 GitHub)


前言

  • 線程池 是 Java 并發(fā)編程中非常重要的概念,同時(shí)也是面試重點(diǎn)考察的知識(shí)點(diǎn)之一「敲黑板」;
  • 在這篇文章里,我將重點(diǎn)分析 線程池工作機(jī)制 & 注意事項(xiàng)。如果能幫上忙,請(qǐng)務(wù)必點(diǎn)贊加關(guān)注,這真的對(duì)我非常重要。

目錄


1. 前置知識(shí)

這篇文章的內(nèi)容會(huì)涉及以下前置 / 相關(guān)知識(shí),貼心的我都幫你準(zhǔn)備好了,請(qǐng)享用~


2. 線程池概述

2.1 為什么要使用線程池?

  • 1、降低資源消耗: 線程是稀缺資源,如果無限制 / 重復(fù)創(chuàng)建,會(huì)消耗系統(tǒng)資源;

  • 2、提高響應(yīng)速度: 通過復(fù)用線程來執(zhí)行任務(wù),可以縮短創(chuàng)建和銷毀線程的時(shí)間;

  • 3、提高線程的可管理性: 使用線程池可以對(duì)線程進(jìn)行統(tǒng)一分配、調(diào)優(yōu)和監(jiān)控。

2.2 線程池如何實(shí)現(xiàn)線程復(fù)用?

線程執(zhí)行完任務(wù)之后,調(diào)用阻塞隊(duì)列BlockingQueue#take()出隊(duì)操作,當(dāng)阻塞隊(duì)列非空時(shí),則繼續(xù)執(zhí)行任務(wù);當(dāng)阻塞隊(duì)列為空時(shí),則當(dāng)前隊(duì)列阻塞。

2.3 提交任務(wù)

向線程池提交任務(wù)可以使用 execute() & submit(),區(qū)別如下:

  • execute(): 用于不需要返回值的任務(wù),無法感知任務(wù)執(zhí)行完成;

  • submit(): 用于需要返回值的任務(wù),通過返回值 Future 可以獲得返回值,如果任務(wù)未執(zhí)行完成,調(diào)用Futrue#get(...) 會(huì)阻塞當(dāng)前線程。

2.4 關(guān)閉線程池

shutdown() / shutdownNow()

線程中斷協(xié)作機(jī)制

2.5 阿里巴巴編程規(guī)范

根據(jù)《阿里巴巴 Java 開發(fā)手冊(cè)》,線程池不允許使用 Executors 去創(chuàng)建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學(xué)更加明確線程池的運(yùn)行規(guī)則,規(guī)避資源耗盡的風(fēng)險(xiǎn)。


3. 線程池相關(guān)類

Executor
ExecutorService
AbstractExecutorService
ThreadPoolExecutor
ScheduledExecutorService
ScheduledThreadPoolExecutor


4. 線程池參數(shù)

ThreadPoolExecutor.java

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    ...
}
參數(shù) 描述
1、int corePoolSize 核心線程數(shù)
2、int maximunPoolSize 最大線程數(shù)
3、long keepAliveTime 線程空閑最大存活時(shí)間
4、BlockingQueue workQueue 阻塞隊(duì)列
5、ThreadFactory threadFactory 線程工廠
6、RejectedExecutionHandler handler 拒絕策略

4.1 int corePoolSize(核心線程數(shù))

如果當(dāng)前線程數(shù)等于 corePoolSize,繼續(xù)提交任務(wù)將進(jìn)入阻塞隊(duì)列中等待。調(diào)用 prestartAllCoreThreads() 可以提前啟動(dòng)所有核心線程;

4.2 int maximunPoolSize(最大線程數(shù))

如果阻塞隊(duì)列滿,繼續(xù)提交任務(wù)將創(chuàng)建新線程,最多可以存在 maximunPoolSize 個(gè)線程;

4.3 long keepAliveTime(線程空閑最大存活時(shí)間)

在線程池空閑時(shí)線程繼續(xù)存活的時(shí)間。注意:keepAliveTime 只有線程數(shù)大于 corePoolSize 才有效;

4.4 BlockingQueue(阻塞隊(duì)列)

如果當(dāng)前線程數(shù)等于 corePoolSize,繼續(xù)提交任務(wù)將進(jìn)入阻塞隊(duì)列中等待。線程池中的阻塞隊(duì)列應(yīng)盡量使用有界隊(duì)列,使用無界隊(duì)列會(huì)導(dǎo)致影響線程池的工作機(jī)制,原因:

  • 1、使用無界隊(duì)列,意味著阻塞隊(duì)列永遠(yuǎn)不會(huì)占滿,maximunPoolSize 和 keepAliveTime 是無效的;
  • 2、無界隊(duì)列有可能造成系統(tǒng)資源耗盡,同時(shí)即使使用有界隊(duì)列,也要盡量控制在合理范圍內(nèi)。

4.5 ThreadFactory threadFactory(線程工廠)

用于獲取 Thread 對(duì)象的實(shí)例,Executors 中默認(rèn)的線程工廠的線程命名規(guī)則為:pool-「線程池計(jì)數(shù)」-thread-「線程計(jì)數(shù)」

Executors.java

namePrefix = "pool-" + poolNumber.getAndIncrement() +  "-thread-";
Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);

4.6 RejectedExecutionHandler(拒絕策略 )

如果阻塞隊(duì)列滿,且線程數(shù)到達(dá)最大值 maximunPoolSize,繼續(xù)提交任務(wù)則會(huì)觸發(fā)拒絕策略 RejectedExecutionHandler#rejectedExecution(...)

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

線程池提供了以下 4 種拒絕策略:

拒絕策略 描述
AbortPolicy 拋出 RejectedExecutionException 異常(默認(rèn))
CallerRunsPolicy 直接在調(diào)用線程執(zhí)行
DiscardOldestPolicy 丟棄阻塞隊(duì)列中隊(duì)首的任務(wù)
DiscardPolicy 直接丟棄當(dāng)前任務(wù)

5. 線程池工作機(jī)制

線程池的工作機(jī)制指的是向線程池提交任務(wù)時(shí),線程池內(nèi)部對(duì)任務(wù)的調(diào)度流程。這部分內(nèi)容是線程池最核心的內(nèi)容,也是面試重點(diǎn)。

  • 1、如果當(dāng)前運(yùn)行的線程少于 corePoolSize,則「創(chuàng)建新的線程」來執(zhí)行任務(wù)(注意,執(zhí)行這一步驟需要獲取全局鎖);
  • 2、如果運(yùn)行的線程等于或多于 corePoolSize,則將任務(wù)「加入 BlockingQueue」;
  • 3、如果 BlockingQueue 已滿,則「創(chuàng)建新的線程」來處理任務(wù);
  • 4、如果創(chuàng)建新線程將使當(dāng)前運(yùn)行的線程超出 maximumPoolSize,將「觸發(fā)拒絕策略」并調(diào)用RejectedExecutionHandler#rejectedExecution()方法。

為什么先將任務(wù)加入阻塞隊(duì)列,而不是線程池滿了再加入阻塞隊(duì)列?
Editting...


6. 如何合理配置線程池?

線程池的配置需要根據(jù)「任務(wù)特性」選擇不同的任務(wù)配置,因地制宜。主要從以下角度分析:

6.1 性質(zhì)

  • 任務(wù)的性質(zhì)分為:CPU 密集型、IO 密集型和混合型。

  • 對(duì)于 CPU 密集型任務(wù),CPU 負(fù)載已經(jīng)非常高了,應(yīng)配置盡可能小的線程,經(jīng)驗(yàn)值為 Ncpu + 1(調(diào)用 Runtime.getRuntime().availableProcessors() 獲得可用的核心數(shù)。);

  • 對(duì)于 IO 密集型任務(wù)(如 磁盤 / 網(wǎng)絡(luò) IO),磁盤或網(wǎng)絡(luò)的讀取速度是遠(yuǎn)遠(yuǎn)小于 CPU 執(zhí)行速度的,為了避免 CPU 出現(xiàn)空閑,應(yīng)配置較多的線程,經(jīng)驗(yàn)值為 Ncpu * 2;

  • 對(duì)于混合型任務(wù),則將其拆分為一個(gè) CPU 密集型任務(wù)和 IO 密集型任務(wù),分別到上述兩種線程池執(zhí)行。需要注意的是,如果兩種拆分的兩個(gè)任務(wù)執(zhí)行時(shí)間相差太大,則應(yīng)該視為一種非混合型任務(wù)。

為什么 CPU 密集型線程池經(jīng)驗(yàn)值為 Ncpu + 1?

在操作系統(tǒng)中,會(huì)將磁盤的一部分空間劃分為虛擬內(nèi)存(讀寫速度慢),當(dāng) CPU 需要訪問的數(shù)據(jù)在虛擬內(nèi)存上時(shí),當(dāng)前線程就進(jìn)入了 “頁缺失” 狀態(tài),需要等待數(shù)據(jù)從磁盤調(diào)度到真實(shí)內(nèi)存才會(huì)喚醒。為了防止出現(xiàn) “頁缺失” 時(shí),CPU 空閑出來,保證任意時(shí)刻 CPU 都不會(huì)空閑,可以選擇 + 1;

6.2 優(yōu)先級(jí)

需要區(qū)分任務(wù)優(yōu)先級(jí),則使用 PriorityBlockingQueue;

6.3 執(zhí)行耗時(shí)

執(zhí)行時(shí)間不同的任務(wù)可以交給不同規(guī)模的線程池來處理,或者可以使用優(yōu)先級(jí)隊(duì)列,讓執(zhí)行時(shí)間短的任務(wù)先執(zhí)行;

6.4 建議使用有界隊(duì)列

無界隊(duì)列沒有限制隊(duì)列元素個(gè)數(shù),有可能有造成資源耗盡。


7. 總結(jié)


創(chuàng)作不易,你的「三連」是丑丑最大的動(dòng)力,我們下次見!

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

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

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