線程池的使用場景和工作原理
近期工作中遇到線程池參數配置不當引發的相關問題。基於此,本文主要結合線程池的原理、使用場景、參數配置、使用注意事項等詳細闡述。文章較長,預計閱讀時長 8~10 分鐘,建議收藏。
前置知識
-
池化技術
一種資源管理策略,旨在提高資源利用率和系統性能,減少資源創建和銷燬的開銷。如數據庫連接池、線程池、常量池等。 -
線程和進程
進程就像是你打開的 360 安全衛士這個軟件本身。線程:在這個空間裏,360 安全衛士可以運行它的各種功能,比如病毒掃描、垃圾清理等都是一個線程在處理。 -
多線程
在計算機中,CPU(中央處理器)是執行線程的地方。在多核 CPU 的系統中,每個核心可以獨立執行線程。即多個線程可以真正地同時運行。
簡單瞭解幾個關鍵詞,我們說說什麼是線程池?
1. 什麼是線程池?
線程池是一種線程管理和複用
的機制。
其核心思想是:預先創建一定數量的線程,並把它們保存在線程池中,用的時候拿出來,用完了丟進去。而不是每次有任務過來重新創建新的線程。如下圖,其核心組成:
-
工作線程(Worker)
:這些是線程池中實際執行任務的線程。 -
任務隊列(Task Queue)
:這是一個存放待執行任務的隊列,工作線程會從這個隊列中取出任務來執行。 -
線程管理器(Thread Manager)
:負責管理線程的生命週期,包括創建新線程、監控線程狀態以及銷燬不再需要的線程。 -
線程工廠(Thread Factory)
:用於創建新線程的組件,可以定製線程的名稱、優先級等屬性。 -
拒絕策略(Rejection Policy)
:當任務隊列滿了,且工作線程都在忙碌時,新提交的任務將被拒絕,拒絕策略定義瞭如何處理這些被拒絕的任務。 -
調度器(Scheduler)
:雖然不是所有線程池都有,但有些線程池會包含調度器,用於安排任務在特定時間執行。
架構圖
2. 線程池有什麼好處?
線程池的優勢總結爲以下幾點:
- 減少資源消耗:
- 線程池通過預先創建線程並複用它們,減少了頻繁創建和銷燬線程所帶來的系統資源消耗。
- 提高系統性能:
- 通過合理配置線程池大小,可以最大限度地壓榨多核 CPU 的性能,從而提高機器的處理能力。
- 提高響應速度:
- 線程池中的線程是預先創建的,這意味着當新任務到達時,可以立即被處理,而不需要等待線程的創建,這樣可以顯著提高任務的響應速度。
- 增強併發性能:
- 線程池支持多線程併發執行任務,這樣可以同時處理多個任務,增強系統的併發處理能力。
線程池有哪些使用場景?
線程池是一種多線程管理工具,它提供了線程的複用和調度功能,可以顯著提高程序的併發性能和資源利用率。以下是線程池的一些常見使用場景:
- 文件上傳下載
- 如:導入、導出功能、文件批數據處理等。
- 異步任務處理
- 如發送郵件、日誌記錄等.
- 定時任務
- 如定時備份、定時清理緩存等,日文件、月文件數據同步等
- 批處理
- 如數據分析、統計報表生成等。
- 快速響應用戶請求
- 如用戶查詢商品詳情頁,可以考慮使用線程池併發地查詢價格、優惠、庫存等信息,再聚合結果返回,降低接口總響應時間。
線程池是如何工作的?
曾經有面試官這樣問:核心線程數 10,最大線程數 20,阻塞隊列最大 100,假如我有 100 個線程同時進來,線程處理任務時間平均按 1s 計算,那理想情況下多長時間可以處理完?
基於上邊的問題,線程池的工作原理可以概括爲以下幾個步驟:
- 工作原理(簡易版)
- 任務提交:
- 開發人員使用
ThreadPoolExecutor
的submit()
方法提交需要執行的任務。這些任務通常是實現了Callable
或Runnable
接口的對象。
- 狀態檢查:
- 線程池會檢查自身的運行狀態。如果線程池不是處於
RUNNING
狀態,那麼會直接拒絕新提交的任務。
- 任務封裝:
- 被提交的任務會被封裝成一個
FutureTask
對象。FutureTask
實現了Future
接口,用來獲取任務的執行結果。
- 核心線程處理:
- 如果線程池的核心線程數小於
corePoolSize
,線程池會嘗試創建一個新的核心線程來執行這個任務。
- 任務隊列處理:
- 如果核心線程數已經達到
corePoolSize
,則任務會被放入一個任務隊列中,等待工作線程從隊列中取出並執行。
- 非核心線程處理:
- 如果任務隊列已滿,並且當前線程池中的線程數量小於
maximumPoolSize
,則線程池會嘗試創建新的非核心線程來執行任務。
- 拒絕策略處理:
- 如果線程池中的線程數量已經達到
maximumPoolSize
,並且任務隊列也已滿,線程池將根據設定的拒絕策略來處理新提交的任務。
- 任務執行與結果獲取:
- 任務執行完成後,線程池會返回一個
Future
對象。通過這個Future
對象,可以查詢任務是否完成,以及獲取任務的執行結果。
總之,歸納起來的執行流程:
線程池參數如何設置?
《阿里巴巴 Java 開發手冊》中規約中強調:
因此實際工作中,我們通常自定義線程池。其核心參數:
public ThreadPoolExecutor(
int corePoolSize, // 核心線程數,線程池中始終保持的線程數,即使它們處於空閒狀態
int maximumPoolSize, // 最大線程數,線程池中允許的最大線程數
long keepAliveTime, // 非核心線程空閒存活時間,當線程池中正在運行的線程數量超過了核心線程數時,多餘的線程在空閒時間達到這個值後會被終止
TimeUnit unit, // 存活時間單位,與keepAliveTime一起使用,表示keepAliveTime的時間單位
BlockingQueue<Runnable> workQueue, // 工作隊列,用於存放待執行任務的阻塞隊列
ThreadFactory threadFactory, // 線程工廠,用於創建新線程
RejectedExecutionHandler handler // 拒絕策略,當任務太多,無法被線程池及時處理時,採取的策略
)
參數最佳實踐:
實際上,大都數公司的線程池配置依照自身業務場景和機器性能配置的。這裏介紹的只是個參考,能說清楚利弊就好
corePoolSize
, // 核心線程數
-
CPU 密集型 可以將線程數設置爲
N(CPU 核數)+1
,比 CPU 核心數多出來的一個線程是爲了防止線程偶發的缺頁中斷。 -
IO 密集型 系統會用大部分的時間來處理 I/O 交互,而線程在處理 I/O 的時間段內不會佔用 CPU 來處理,這時就可以將 CPU 交出給其它線程使用。因此在 I/O 密集型任務的應用中,我們可以多配置一些線程,具體的計算方法可以是 :
核心線程數=CPU核數*2
。
如何理解 CPU 密集 和 I/O 密集?
CPU 密集型任務通常在 CPU 上執行進行大量計算(如 RSA 加密) I/O 密集多指 I/O 操作(主要磁盤讀寫,如 數據庫操作等)
附加:如何獲取 CPU 核數?
代碼:
Runtime.getRuntime().availableProcessors();
命令:
$ lscpu
maximumPoolSize
// 最大線程數
具體依據服務器的 I/O 性能,經驗法則:
對於 I/O 密集型應用,通常將 maximumPoolSize 設置爲可用處理器核心數的 5 到 10 倍。
文件處理服務:如果服務需要處理大量的文件上傳和下載,maximumPoolSize 可以設置爲 20 或更多。
keepAliveTime
// 非核心線程空閒存活時間
看業務實時性高不高,一般系統,設置 60s 亦可
unit
// 存活時間單位
看業務實時性高不高,一般系統,設置 s 亦可
workQueue
// 工作隊列
按目前經驗,工作中常用:
LinkedBlockingQueue 是一個無界隊列,適用於任務數量可能突然激增的場景;
ArrayBlockingQueue 是一個有界隊列,適用於需要限制最大任務數量的場景,以避免資源耗盡
-
threadFactory
// 線程工廠 一般使用默認的也可 -
handler
// 拒絕策略
主要有 4 種拒絕策略:
AbortPolicy:直接丟棄任務,拋出異常,這是默認策略
CallerRunsPolicy:只用調用者所在的線程來處理任務
DiscardOldestPolicy:丟棄等待隊列中最舊的任務,並執行當前任務
DiscardPolicy:直接丟棄任務,也不拋出異常
介於此,給出公司中用到的其中一個案例配置,僅供參考。
@Bean("XXXAsyncPool")
public Executor myTaskAsyncPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//配置核心線程數
executor.setCorePoolSize(10);
//配置核心線程數
executor.setMaxPoolSize(20);
//配置隊列容量
executor.setQueueCapacity(1000);
//設置線程活躍時間
executor.setKeepAliveSeconds(60);
//設置線程名
executor.setThreadNamePrefix("XXXAsyncPool-");
//設置拒絕策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
拒絕策略先前遇到坑,
篇幅,稍後單獨做個介紹。
參考資料
-
https://mp.weixin.qq.com/s/dq1fOfy8Uk0folqFd_fBNw
-
https://mp.weixin.qq.com/s/Eh4SM5lStcLLdXaY5ENr-Q
-
https://mp.weixin.qq.com/s/icrrxEsbABBvEU0Gym7D5Q
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/G7djDLyUdbkA0WMm2hnwOA