執行緒池是很常用的併發框架,幾乎所有需要非同步和併發處理任務的程式都可用到執行緒池。
使用執行緒池的好處如下:
- 降低資源消耗:可重複利用已建立的執行緒池,降低建立和銷燬帶來的消耗;
- 提高響應速度:任務到達時,可立即執行,無需等待執行緒建立;
- 提高執行緒的可管理性:執行緒池可對執行緒統一分配、調優和監控。
原理
執行緒池的原理非常簡單,這裡用處理流程來概括:
- 執行緒池判斷核心池裡的執行緒是否都在執行任務,如果不是,建立一個新的執行緒來執行任務;
- 如果核心執行緒池已滿,則將新任務存在工作佇列中;
- 如果工作佇列滿了,執行緒數量沒有達到執行緒池上限的前提下,新建一個執行緒來執行任務;
- 執行緒數量達到上限,則觸發飽和策略來處理這個任務;
使用工作佇列,是為了儘可能降低執行緒建立的開銷。工作佇列用阻塞佇列來實現。
阻塞佇列
阻塞佇列(BlockingQueue)是指支援阻塞的插入和移除元素的佇列。
- 阻塞的插入:當佇列滿時,阻塞插入元素的執行緒,直到佇列不滿;
- 阻塞的移除:當佇列為空,阻塞移除元素的線層,直到佇列不為空;
原理:使用通知者模式實現。當生產者往滿的佇列中新增元素時,會阻塞生產者。消費者移除元素時,會通知生產者當前佇列可用。
阻塞佇列有以下三種型別,分別是:
- 有界阻塞佇列:ArrayBlockingQueue(陣列),LinkedBlockingQueue(連結串列)
- 無界阻塞佇列:LinkedTransferQueue(連結串列),PriorityBlockingQueue(支援優先順序排序),DelayQueue(支援延時獲取元素的無界阻塞佇列)
- 同步移交佇列:SynchronousQueue
有界阻塞佇列
主要包括ArrayBlockingQueue(陣列),LinkedBlockingQueue(連結串列)兩種。有界佇列大小與執行緒數量大小相互配合,佇列容量大執行緒數量小時,可減少上下文切換降低cpu使用率,但是會降低吞吐量。
無界阻塞佇列
比較常用的是LinkedTransferQueue。FixedThreadPool就是用這個實現的。無界阻塞佇列要慎重使用,因為在某些情況,可能會導致大量的任務堆積到佇列中,導致記憶體飆升。
同步移交佇列
SynchronousQueue。不儲存元素的阻塞佇列,每一個put操作必須等待一個take操作,否則不能繼續新增元素。用於實現CachedThreadPool執行緒池。
各個執行緒池所使用的任務佇列對映關係如下:
執行緒池 | 阻塞佇列 |
---|---|
FixedThreadPool | LinkedBlockingQueue |
SingleThreadExecutor | LinkedBlockingQueue |
CachedThreadExecutor | SynchronousQueue |
ScheduledThreadPoolExecutor | LinkedBlockingQueue |
實現類分析
ThreadPoolExecutor是Java執行緒池的實現類,是Executor介面派生出來的最核心的類。依賴關係圖如下:
這裡不得不提到Executor框架,該框架包含三大部分,如下:
- 任務。被執行任務需要實現的介面:Runnable和Callable;
- 任務執行。即上述核心介面Executor以及繼承而來的ExecutorService。ExecutorService派生出如下兩個類:
- ThreadPoolExecutor:執行緒池核心實現類;
- ScheduledThreadPoolExecutor:用來做定時任務;
- 非同步計算的結果。介面Future和實現Future介面的FutureTask類。
執行緒池建立
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds, runnableTaskQueue, handler)
構造方法如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
引數說明:
- corePoolSize:核心池的執行緒數量;
- workQueue:用於儲存任務的工作佇列;
- maximumPoolSize:最大執行緒池的大小;
- keepAliveTime:當執行緒數量大於核心池執行緒數量時,keepAliveTime為多餘的空閒執行緒等待新任務的最長時間,超過這個時間,多餘的執行緒會被終止;
- TimeUnit:keepAliveTime的單位;
- ThreadFactory:執行緒工廠,可以給執行緒設定名字;
- handler:飽和策略。當佇列和執行緒池都滿了,會觸發飽和策略,來處理新提交的任務。飽和策略以下幾種:
- AbortPolicy:直接丟擲異常;
- CallerRunsPolicy:只用呼叫者所線上程來執行任務;
- DiscardOldestPolicy:丟棄最近一個任務並執行當前任務;
- DiscardPolicy:不處理,丟棄掉。
使用Executors建立執行緒池
使用工具類Executors可建立三種型別的執行緒池:FixedThreadPool、SingleThreadExecutor、CachedThreadPool。本質上也是呼叫上述構造方法。理解了前文的引數解釋,下面三種執行緒池也就容易理解了。
- FixedThreadPool
可重用固定執行緒數的執行緒池。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
工作流程如下:
- 如果當前執行的執行緒數少於corePoolSize,則建立新執行緒來執行任務;
- 執行緒數等於corePoolSize之後,新任務加入LinkedBlockingQueue(無界阻塞佇列)。因為最大執行緒數maximumPoolSize引數值等於corePoolSize,不會產生多餘執行緒;
- 執行緒執行完任務之後會反覆從LinkedBlockingQueue中獲取任務來執行。
- SingleThreadExecutor
單個worker執行緒的執行緒池
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
SingleThreadExecutor與FixedThreadPool的區別在於,maximumPoolSize和corePoolSize都設定成了1,其它引數都一樣。
- CachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
CachedThreadPool將corePoolSize設定為0,maximumPoolSize設定為無限大,同時使用了一個沒有容量的工作佇列SynchronousQueue。這個執行緒池沒有固定的核心執行緒,而是根據需要建立新執行緒。
工作流程:
- 有新任務時,主執行緒執行SynchronousQueue.offer操作,空閒執行緒執行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)操作,配對成功則將任務交給空閒執行緒執行;
- 當沒有空閒執行緒時,上面的配對操作失敗,此時會建立一個新執行緒來執行任務;
- 任務執行完畢後,空閒執行緒會等待60秒。60秒內如果有新任務,就立即執行,否則時間一過執行緒就終止。
執行緒池關閉
呼叫shutdown或者shutdownNow方法可關閉執行緒池。原理是遍歷執行緒池中所有工作執行緒,呼叫interrupt方法來中斷執行緒。
- shutdown:將執行緒置為SHUTDOWN狀態,不能接受新的任務,等待所有任務執行完畢;
- shutdownNow:將執行緒置為STOP狀態,不能接受新的任務,嘗試去終止正在執行的惡任務;
這裡涉及到ThreadPoolExecutor中定義的執行緒的五種狀態
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
- RUNNING:接受新任務,處理任務;
- SHUTDOWN:不接受新任務,但會把佇列中任務處理完;
- STOP:不接受新任務,不處理佇列中的任務,並且終止正在處理的任務;
- TIDYING:正在執行的任務和佇列都為空,進入該狀態,將要執行terminated();
- TERMINATED:所有terminated()方法執行完畢,執行緒池徹底終止。
當佇列和正在執行的任務都為空時,由SHUTDOWN轉化為TIDYING;當正在執行的任務為空,由STOP轉化為TIDYING。
本部落格從執行緒池的原理介紹作為切入點,分析了執行緒池中尤為關鍵的元件:阻塞佇列。同時分析了執行緒池的核心實現類ThreadPoolExecutor。以執行緒池的建立和關閉的思路,梳理了相關知識點,包括三種常用執行緒池介紹以及執行緒池五種狀態。