阿里巴巴Java手冊中,關於線程池:
- 線程資源必須通過線程池提供,不允許在應用中自行顯示創建線程。
- 使用線程池的好處,是減少在創建和銷燬線程上所花的時間以及系統資源的開銷,解決資源不足的問題。
- 如果不使用線程池,有可能造成系統創建大量同類線程而導致消耗完內存或者“過度切換”的問題。
線程池的好處:
- 可以重用線程,避免線程創建的開銷;
- 任務過多時,通過排隊避免創建過多線程,減少系統資源消耗和競爭,確保任務有序完成。
一、JUC線程池詳解
Java JUC包中的實現類是ThreadPoolExecutor,繼承AbstractExecutorService,實現了ExecutorService。
ThreadPoolExecutor比較重要的兩個構造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
其中:
- corePoolSize : 核心線程數量
- maximumPoolSize : 最大線程數量
- keepAliveTime / unit : 空閒線程存活時間
- workQueue : 任務隊列
- threadFactory : 線程工廠
- handler : 拒絕策略
1、corePoolSize,核心線程數量,剛創建一個線程池後,不會創建任何線程。
當有新任務到來時,如果當前線程數量小於corePoolSize,會創建一個新線程執行該任務,即使其他線程是空閒的,也會創建新線程。如果線程數量大於corePoolSize,不會立即創建新線程,而是嘗試排隊,如果因爲隊列滿了或其他原因不能立即入隊,就不會排隊,而是檢查線程個數是否達到了maximumPoolSize,如果沒有達到,繼續創建新線程,直到線程數達到maximumPoolSize。
流程圖
核心線程:當線程個數小於corePoolSize時的線程。
核心線程默認行爲:
- 不會預先創建,只有當有任務時纔會創建。
- 不會因爲空閒而被終止,keepAliveTime參數不適用核心線程。
改變這些默認行爲ThreadPoolExecutor有如下方法:
// 預先創建所有核心線程
public int prestartAllCoreThreads();
// 創建一個核心線程,如果所有核心線程都已經創建,返回false
public boolean prestartCoreThread();
// 參數爲true時,允許keepAliveTime適用於核心線程
public void allowCoreThreadTimeOut(boolean value)
2、keepAliveTime,目的是爲了釋放多餘的線程資源。當線程池中線程個數大於corePoolSize時額外空閒線程的存活時間。一個非核心線程,在空閒等待新任務的最長等待時間。0表示所有線程都不會超時終止。
3、workQueue,阻塞隊列BlockingQueue:
- LinkedBlockingQueue:基於鏈表,可以指定最大長度,默認無界
- ArrayBlockingQueue:基於數組,有界
- PriorityBlockingQueue:基於堆,無界阻塞優先級隊列
- SynchronousQueue:沒有實際存儲空間的同步阻塞隊列。
對於無界隊列,線程個數最多隻能達到corePoolSize,達到corePoolSize後,新任務總會排隊,maximumPoolSize就沒有意義了。
對於SynchronousQueue,當嘗試排隊時,只有正好有空閒線程在等待接受任務時,纔會入隊成功,否則,總是會創建新線程,直到maximumPoolSize。
4、handler,RejectedExecutionHandler,任務拒絕策略
隊列有限,並且maximumPoolSize有限,當隊列排滿,線程個數也達到maximumPoolSize,此時新任務會觸發線程池的任務拒絕策略。
ThreadPoolExecutor實現了4種處理方式:
- ThreadPoolExecutor.AbortPolicy:默認方式,拋出異常
- ThreadPoolExecutor.DiscardPolicy:靜默處理,忽略新任務,不拋出異常,也不執行
- ThreadPoolExecutor.DiscardOldestPolicy:將等待時間最長的任務扔掉,然後新任務入隊
- ThreadPoolExecutor.CallerRunPolicy:在任務提交者線程中執行新任務,而不是交給線程池的線程執行。
拒絕策略只有在隊列有界,maximumPoolSize有限的情況下才會觸發。
如果隊列無界,服務不了的任務總是會排隊;請求隊列可能會消耗非常大的內存,甚至引發OOM;
如果隊列有界但maximumPoolSize無限,可能會創建過多的線程,佔滿CPU和內存,使得任何任務都難以完成。
在任務量非常大的場景中,需要讓拒絕策略有機會執行。
5、threadFactory,線程工廠
ThreadPoolExecutor的默認實現是Executors類中的靜態內部類DefaultThreadFactory。
創建一個線程,設置默認名稱(pool-線程池編號-thread-線程編號),設置daemon屬性爲false,設置線程優先級爲標準默認優先級(5)。
如果要自定義線程屬性,可以實現自定義的ThreadFactory。
二、工廠類Executors
雖然不推薦直接使用Executors工廠類創建線程池,但還是要了解一下利弊。
1.newSingleThreadExecutor
只使用一個線程,使用無界隊列,線程創建後不會超時,順序執行所有任務。
適用於需要保證所有任務被順序執行的場合。
無界隊列,如果排隊任務過多,可能會消耗過多的內存。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
2.newFixedThreadPool
使用固定樹木的線程,使用無界隊列,線程創建後不會超時終止。
無界隊列,如果排隊任務過多,可能會消耗過多的內存。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
3.newCachedThreadPool
核心線程數爲0,最大線程數爲Integer的最大值,線程空閒時間爲60秒,隊列爲SynchronousQueue。
當新任務提交,正好有空閒線程在等待任務,則空閒線程接受該任務,否則總是創建新線程。對任一空閒線程60s內沒有新任務則終止。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
使用場景對比:
- 系統負載很高 - newFixedThreadPool
- newFixedThreadPool,通過隊列對新任務排隊,保證有足夠的資源處理實際任務;
- newCachedThreadPool,爲每個任務創建一個線程,導致創建過多的線程,競爭CPU和內存資源;
- 系統負載不太高,單個任務執行時間比較短 - newCachedThreadPool
- newCachedThreadPool,效率可能更高,因爲任務可以不經排隊,直接交給一個空閒線程或新建線程。
- 系統負載可能極高 - 兩者都不是最好的選擇,應根據具體情況自定義合適的參數。
- newFixedThreadPool,隊列過長
- newCachedThreadPool,線程過多
- CPU密集型任務(計算型任務),一般線程數量爲CPU數量的1~2倍,過多線程可能增大上下文切換的開銷。
- IO密集型任務,相對比CPU密集型任務,需要多一些線程,根據具體的IO阻塞時長進行考量決定。如tomcat,默認最大線程數爲200。
網上的帖子質量參差不齊,甚至有些前後矛盾,在下的文章都是學習過程中的總結,如果發現錯誤,歡迎留言指出~