前言
線程池內部是多個線程的集合,在創建初期,線程池會創建出多個空閒的線程,當有一個任務需要執行時,線程池會選擇出一個線程去執行它,執行結束後,該線程不會被銷燬,而是可以繼續複用。
使用線程池可以大大減少線程頻繁創建與銷燬的開銷,降低了系統資源的消耗。當任務來臨時,直接複用之前的線程,而不是先創建,提高了系統的響應速度。此外,線程池可以控制最大的併發數,避免資源的過度消耗。
簡單實例
先給出一個線程池的簡單例子:
package com.xue.testThreadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(2);
for (int i = 0; i < 4; i++) {
int finalI = i;
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在執行任務" + finalI);
}
});
}
threadPool.shutdown();
}
}
輸出如下:
可見,2個線程總共執行了4個任務,線程得到了複用。
線程池的核心參數
這些核心參數位於ThreadPoolExecutor的構造方法中:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize 核心線程數,或者說常駐線程數,線程池中最少線程數
- maximumPoolSize 最大線程數
- keepAliveTime 空閒線程的存活時間,線程池中當前線程數大於corePoolSize時,那些空閒時間達到keepAliveTime的空閒線程,它們將會被銷燬掉
- TimeUnit keepAliveTime的時間單位
- workQueue 任務隊列,存放未被執行的任務
- threadFactory 創建線程的工廠
- handler 拒絕策略,當前線程數≥最大線程數且任務隊列滿的時候,對後續任務的拒絕方式
線程池的種類
不同的線程池有不同的適用場景,本質上都是在Executors類中實例化一個ThreadPoolExecutor對象,只是傳入的參數不一樣罷了。
線程池的種類有以下幾種:
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
創建一個固定大小的線程池,即核心線程數等於最大線程數,每個線程的存活時間和線程池的壽命一致,線程池滿負荷運作時,多餘的任務會加入到無界的阻塞隊列中,newFixedThreadPool可以很好的控制線程的併發量。
newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
創建一個可以無限擴大的線程池,當任務來臨時,有空閒線程就去執行,否則立即創建一個線程。當線程的空閒時間超過1分鐘時,銷燬該線程。適用於執行任務較少且需要快速執行的場景,即短期異步任務。
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
創建一個大小爲1的線程池,用於順序執行任務。
newScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
創建一個初始大小爲corePoolSize的線程池,線程池的存活時間沒有限制,newScheduledThreadPool中的schedule方法用於延時執行任務,scheduleAtFixedRate用於週期性地執行任務。
線程池執行任務的流程
當線程池中線程數小於corePoolSize時,新提交任務將創建一個新線程執行任務,即使此時線程池中存在空閒線程。
當線程池中線程數達到corePoolSize時,新提交任務將被放入workQueue中,等待線程池中任務調度執行 。
當workQueue已滿,且maximumPoolSize > corePoolSize時,新提交任務會創建新線程執行任務。
當workQueue已滿,且提交任務數超過maximumPoolSize,任務由RejectedExecutionHandler處理。
當線程池中線程數超過corePoolSize,且超過這部分的空閒時間達到keepAliveTime時,回收這些線程。
當設置allowCoreThreadTimeOut(true)時,線程池中corePoolSize範圍內的線程空閒時間達到keepAliveTime也將回收。
使用更加直觀的流程圖來描述:
注:此章節參考通俗易懂,各常用線程池執行的-流程圖
工作隊列
工作隊列用來存儲提交的任務,工作隊列一般使用的都是阻塞隊列。阻塞隊列可以保證任務隊列中沒有任務時阻塞獲取任務的線程,使得線程進入wait狀態,釋放cpu資源。當隊列中有任務時才喚醒對應線程從隊列中取出消息進行執行。
阻塞隊列一般由以下幾種:
LinkedBlockingQueue
由單鏈表實現的無界阻塞隊列,遵循FIFO。注意這裏的無界是因爲其記錄隊列大小的數據類型是int,那麼隊列長度的最大值就是恐怖的Integer.MAX_VALUE,這個值已經很大了,因此可以將之稱爲無界隊列。不過該隊列也提供了有參構造函數,可以手動指定其隊列大小,否則使用默認的int最大值。
LinkedBlockingQueue只能從head取元素,從tail添加元素。添加元素和獲取元素都有獨立的鎖,也就是說它是讀寫分離的,讀寫操作可以並行執行。LinkedBlockingQueue採用可重入鎖(ReentrantLock)來保證在併發情況下的線程安全。
當線程數目達到corePoolSize時,後續的任務會直接加入到LinkedBlockingQueue中,在不指定其隊列大小的情況下,該隊列永遠也不會滿,可能內存滿了,隊列都不會滿,此時maximumPoolSize和拒絕策略將不會有任何意義。
ArrayBlockingQueue
由數組實現的有界阻塞隊列,同樣遵循FIFO,必須制定隊列大小。使用全局獨佔鎖的方式,使得在同一時間只有一個線程能執行入隊或出隊操作,相比於LinkedBlockingQueue,ArrayBlockingQueue鎖的力度很大。
SynchronousQueue
是一個沒有容量的隊列,當然也可以稱爲單元素隊列。會將任務直接傳遞給消費者,添加任務時,必須等待前一個被添加的任務被消費掉,即take動作等待put動作,put動作等待take動作,put與take是循環往復的。
如果線程拒絕執行該隊列中的任務,或者說沒有線程來執行。那麼舊任務無法被執行,新任務也無法被添加,線程池將陷入一種尷尬的境地。因此,該隊列一般需要maximumPoolSize爲Integer.MAX_VALUE,有一個任務到來,就立馬新起一個線程執行,newCachedThreadPool就是使用的這種組合。
關於這些阻塞隊列的源碼解析,可能需要另開篇幅。
線程工廠
先看一下,ThreadPoolExecutor構造方法中默認使用的線程工廠
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
defaultThreadFactory對於線程的命名方式爲“pool-”+pool的自增序號+"-thread-"+線程的自增序號,這也印證了在簡單實例的章節中,輸出Thread.getCurrentThread.getName()是“pool-1-thread-1”的樣式
默認線程工廠給線程的取名沒有太多的意義,在實際開發中,我們一般會給線程取個比較有識別度的名稱,方便出現問題時的排查。
拒絕策略
如果當工作隊列已滿,且線程數目達到maximumPoolSize後,依然有任務到來,那麼此時線程池就會採取拒絕策略。
ThreadPoolExecutor中提供了4種拒絕策略。
AbortPolicy
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
public static class AbortPolicy implements RejectedExecutionHandler {
public AbortPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
這是線程池的默認拒絕策略,直接會丟棄任務並拋出RejectedExecutionException異常。
DiscardPolicy
public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
丟棄後續提交的任務,但不拋出異常。建議在一些無關緊要的場景中使用此拒絕策略,否則無法及時發現系統的異常狀態。
DiscardOldestPolicy
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
從源碼中可以看到,此拒絕策略會丟棄隊列頭部的任務,然後將後續提交的任務加入隊列中。
CallerRunsPolicy
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
由調用線程執行該任務,即提交任務的線程,一般是主線程。
如何配置最大線程數
CPU密集型任務
CPU密集指的是需要進行大量的運算,一般沒有什麼阻塞。
儘量使用較小的線程池,大小一般爲CPU核心數+1。因爲CPU密集型任務使得CPU使用率很高,若開過多的線程數,會造成CPU過度切換。
IO密集型任務
IO密集指的是需要進行大量的IO,阻塞十分嚴重,可以掛起被阻塞的線程,開啓新的線程幹別的事情。
可以使用稍大的線程池,大小一般爲CPU核心數*2。IO密集型任務CPU使用率並不高,因此可以讓CPU在等待IO的時候有其他線程去處理別的任務,充分利用CPU時間。
當然,依據IO密集的程度,可以在兩倍的基礎上進行相應的擴大與縮小。
總結
這篇文章粗淺地說明了線程池的種類、執行流程、工作隊列與拒絕策略等,但缺少對線程池源碼的分析,這個會另開篇幅進行說明。