執行緒池的使用
1、執行緒池的使用場景
等待返回任務的結果的多步驟的處理場景, 批量併發執行任務,總耗時是單個步驟耗時最長的那個,提供整體的執行效率,
最終一致性,非同步執行任務,無需等待,快速返回
2、執行緒池的關鍵引數說明
一般情況下我們是通過ThreadPoolExecutor來構造我們的執行緒池物件的。
* 阿里巴巴的開發規範文件是禁止直接使用Executors靜態工廠類來建立執行緒池的,原因是
【強制】執行緒池不允許使用 Executors 去建立,而是通過 ThreadPoolExecutor 的方式,這樣
的處理方式讓寫的同學更加明確執行緒池的執行規則,規避資源耗盡的風險。
說明: Executors 返回的執行緒池物件的弊端如下:
(1) FixedThreadPool 和 SingleThreadPool :
允許的請求佇列長度為 Integer.MAX_VALUE ,可能會堆積大量的請求,從而導致 OOM 。
(2) CachedThreadPool 和 ScheduledThreadPool :
允許的建立執行緒數量為 Integer.MAX_VALUE ,可能會建立大量的執行緒,從而導致 OOM 。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
}
引數說明:
- corePoolSize:核心執行緒數,執行緒池最低的執行緒數
- maximumPoolSize:允許的最大的執行緒數
- keepAliveTime:當前執行緒數超過corePoolSize的時候,空閒執行緒保留的時間
- unit: keepAliveTime執行緒保留的時間的單位
- workQueue: 任務緩衝區
- threadFactory: 執行緒的構造工廠
- handler: 執行緒池飽含時候的處理策略
3、執行緒池的分類
Java通過Executors提供四種執行緒池,分別為:
- newCachedThreadPool建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒執行緒,若無可回收,則新建執行緒。
- newFixedThreadPool 建立一個定長執行緒池,可控制執行緒最大併發數,超出的執行緒會在佇列中等待。
- newScheduledThreadPool 建立一個定長執行緒池,支援定時及週期性任務執行。
- newSingleThreadExecutor 建立一個單執行緒化的執行緒池,它只會用唯一的工作執行緒來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行。
3.1、newCachedThreadPool
public static ExecutorService newCachedThreadPool(){
return new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.MILLISECONDS,new SynchronousQueue<Runnable>());
}
它是一個可以無限擴大的執行緒池;
- 它比較適合處理執行時間比較小的任務;
corePoolSize為0,maximumPoolSize為無限大,意味著執行緒數量可以無限大;
keepAliveTime為60S,意味著執行緒空閒時間超過60S就會被殺死;
採用SynchronousQueue裝等待的任務,這個阻塞佇列沒有儲存空間,這意味著只要有請求到來,就必須要找到一條工作執行緒處理他,如果當前沒有空閒的執行緒,那麼就會再建立一條新的執行緒。
3.2、newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads){
return new ThreadPoolExecutor(nThreads,nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}
- 它是一種固定大小的執行緒池;corePoolSize和maximunPoolSize都為使用者設定的執行緒數量nThreads;
- keepAliveTime為0,意味著一旦有多餘的空閒執行緒,就會被立即停止掉;但這裡keepAliveTime無效;
- 阻塞佇列採用了LinkedBlockingQueue,它是一個無界佇列;由於阻塞佇列是一個無界佇列,因此永遠不可能拒絕任務;
- 由於採用了無界佇列,實際執行緒數量將永遠維持在nThreads,因此maximumPoolSize和keepAliveTime將無效。
3.3、ScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
- 定時任務的使用
3.4、SingleThreadExecutor
public static ExecutorService newSingleThreadExecutor(){
return new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}
- 它只會建立一條工作執行緒處理任務;
- 採用的阻塞佇列為LinkedBlockingQueue;
3.5、總結
執行緒池 | 特點 | 建議使用場景 |
---|---|---|
newCachedThreadPool | 1、執行緒數無上限 2、空閒執行緒存活60s 3、阻塞佇列 |
1、任務執行時間短 2、任務要求響應時間短 |
newFixedThreadPool | 1、執行緒數固定 2、無界佇列 |
1、任務比較平緩 2、控制最大的執行緒數 |
newScheduledThreadPool | 核心執行緒數量固定、非核心執行緒數量無限制(閒置時馬上回收) | 執行定時 / 週期性 任務 |
newSingleThreadExecutor | 只有一個核心執行緒(保證所有任務按照指定順序在一個執行緒中執行,不需要處理執行緒同步的問題) | 不適合併發但可能引起IO阻塞性及影響UI執行緒響應的操作,如資料庫操作,檔案操作等 |
4、使用執行緒池容易出現的問題
現象 | 原因 |
---|---|
整個系統影響緩慢,大部分504 | 1、為設定最大的執行緒數,任務積壓過多,執行緒數用盡 |
oom | 1、佇列無界或者size設定過大 |
使用執行緒池對效率並沒有明顯的提升 | 1、執行緒池的引數設定過小,執行緒數過小或者佇列過小,或者是伺服器的cpu核數太低 |
5、執行緒池的監控
5.1、為什麼要對執行緒池進行監控
- 執行緒池中執行緒數和佇列的型別及長度對執行緒會造成很大的影響,而且會爭奪系統稀有資源,執行緒數。設定不當,或是沒有最大的利用系統資源,提高系統的整體執行效率,或是導致整個系統的故障。典型的場景是執行緒數被佔滿,其他的請求無響應。或是任務積壓過多,直接oom
- 方便的排查執行緒中的故障以及優化執行緒池的使用
5.2、監控的原理
另起一個定時單執行緒數的執行緒池newSingleThreadScheduledExecutor
呼叫scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)定時執行監控任務;
定時任務內 通過ThreadPoolExecutor物件獲取監控的物件資訊,比如t執行緒池需要執行的任務數、執行緒池在執行過程中已完成的任務數、曾經建立過的最大執行緒數、執行緒池裡的執行緒數量、執行緒池裡活躍的執行緒數量、當前排隊執行緒數
根據預設的日誌或報警策略,進行規則控制
5.3、實現的細節
定義執行緒池並啟動監控
/**
* 定義執行緒池的佇列的長度
*/
private final Integer queueSize = 1000;
/**
* 定義一個定長的執行緒池
*/
private ExecutorService executorService;
@PostConstruct
private void initExecutorService() {
log.info(
"executorService init with param: threadcount:{} ,queuesize:{}",
systemConfig.getThreadCount(),
systemConfig.getThreadQueueSize());
executorService =
new ThreadPoolExecutor(
systemConfig.getThreadCount(),
systemConfig.getThreadCount(),
0,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue(systemConfig.getThreadQueueSize()),
new BasicThreadFactory.Builder()
.namingPattern("async-sign-thread-%d")
.build(),
(r, executor) -> log.error("the async executor pool is full!!"));
/** 啟動執行緒池的監控 */
ThreadPoolMonitoring threadPoolMonitoring = new ThreadPoolMonitoring();
threadPoolMonitoring.init();
}
執行緒池的監控
/**
* 功能說明:執行緒池監控
*
* @params
* @return <br>
* 修改歷史<br>
* [2019年06月14日 10:20:10 10:20] 建立方法by fengqingyang
*/
public class ThreadPoolMonitoring {
/** 用於週期性監控執行緒池的執行狀態 */
private final ScheduledExecutorService scheduledExecutorService =
Executors.newSingleThreadScheduledExecutor(
new BasicThreadFactory.Builder()
.namingPattern("async thread executor monitor")
.build());
/**
* 功能說明:自動執行監控
*
* @return <br>
* 修改歷史<br>
* [2019年06月14日 10:26:51 10:26] 建立方法by fengqingyang
* @params
*/
public void init() {
scheduledExecutorService.scheduleAtFixedRate(
() -> {
try {
ThreadPoolExecutor threadPoolExecutor =
(ThreadPoolExecutor) executorService;
/** 執行緒池需要執行的任務數 */
long taskCount = threadPoolExecutor.getTaskCount();
/** 執行緒池在執行過程中已完成的任務數 */
long completedTaskCount = threadPoolExecutor.getCompletedTaskCount();
/** 曾經建立過的最大執行緒數 */
long largestPoolSize = threadPoolExecutor.getLargestPoolSize();
/** 執行緒池裡的執行緒數量 */
long poolSize = threadPoolExecutor.getPoolSize();
/** 執行緒池裡活躍的執行緒數量 */
long activeCount = threadPoolExecutor.getActiveCount();
/** 當前排隊執行緒數 */
int queueSize = threadPoolExecutor.getQueue().size();
log.info(
"async-executor monitor. taskCount:{}, completedTaskCount:{}, largestPoolSize:{}, poolSize:{}, activeCount:{},queueSize:{}",
taskCount,
completedTaskCount,
largestPoolSize,
poolSize,
activeCount,
queueSize);
/** 超過閥值的80%報警 */
if (activeCount >= systemConfig.getThreadCount() * 0.8) {
log.error(
"async-executor monitor. taskCount:{}, completedTaskCount:{}, largestPoolSize:{}, poolSize:{}, activeCount:{},queueSize:{}",
taskCount,
completedTaskCount,
largestPoolSize,
poolSize,
activeCount,
queueSize);
;
}
} catch (Exception ex) {
log.error("ThreadPoolMonitoring service error,{}", ex.getMessage());
}
},
0,
30,
TimeUnit.SECONDS);
}
}
6、需要注意的事項
- 執行緒數要合理設定,一般建議值是核數的2倍。
- 執行緒池佇列的型別和長度要根據業特性合理設定
- 不同的業務需要執行緒池隔離,避免相互影響
- 未每個執行緒池增加特有的命名規範以及關鍵的日誌,方便出問題排查和優化
7、後續
更多精彩,敬請關注, 程式設計師導航網 https://chenzhuofan.top