執行緒池的好處
Java中的執行緒池是運用場景最多的併發框架,幾乎所有需要非同步或併發執行任務的程式都可以使用執行緒池。在開發過程中,合理地使用執行緒池,相對於單執行緒序列處理(Serial Processing)和為每一個任務分配一個新執行緒(One Task One New Thread)的做法能夠帶來3個好處。
- 降低資源消耗。通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。
- 提高響應速度。當任務到達時,任務可以不需要等到執行緒建立就能立即執行。
- 提高執行緒的可管理性。執行緒是稀缺資源,如果無限制地建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一分配、調優和監控。但是,要做到合理利用執行緒池,必須對其實現原理了如指掌。
執行緒池的實現原理
下面所有的介紹都是基於JDK 1.8原始碼。
架構設計
Java中的執行緒池核心實現類是ThreadPoolExecutor。這個類的設計是繼承了AbstractExecutorService抽象類和實現了ExecutorService,Executor兩個介面,關係大致如下圖所示:
下面將從頂向下逐個介紹這個4個介面與類。
Executor
頂層介面Executor提供了一種將任務提交和每個任務的執行機制(包括執行緒使用的細節以及執行緒排程等)解耦分開的方法。使用Executor可以避免顯式的建立執行緒。例如,對於一系列的任務,你可能會使用下列這種方式來代替new Thread(new(RunnableTask())).start()
的方式:
Executor executor = anExecutor;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
Executor介面提供了一個介面方法,用來在未來的某段時間執行指定的任務。指定的任務
- 可能由一個新建立的執行緒執行;
- 可能由一個執行緒池中空閒的執行緒執行;
- 也可能由方法的呼叫執行緒執行。
這些可能執行方式都取決於Executor介面實現類的設計或實現方式。
public interface Executor {
void execute(Runnable command);
}
Serial Processing
事實上,Executor介面並沒有嚴格的要求執行緒的執行需要非同步進行。最簡單的介面實現方法是,將所有的任務以呼叫方法的執行緒執行。
class DirectExecutor implements Executor {
public void execute(Runnable r) {
r.run();
}
}
這種實際上就是上面提到的Serial Processing的方式。假設,我們現在以這種方式去實現一個響應請求的伺服器應用。那麼,這種實現方式雖然在理論上是正確的。
- 但是其效能卻非常差,因為它每次只能響應處理一個請求。如果有大量請求則只能序列響應。
- 同時,如果伺服器響應邏輯裡面有檔案I/O或者資料庫操作,伺服器需要等待這些操作完成才能繼續執行。這個時候如果阻塞的時間過長,伺服器資源利用率就很低。這樣,在等待過程中,伺服器CPU將處於空閒狀態。
綜上,這種Serial Processing的方式方式就會有無法快速響應問題和低吞吐率問題。
One Task One New Thread
不過,更典型的實現方式是,任務由一些其他的執行緒執行而不是方法呼叫的執行緒執行。例如,下面的Executor的實現方法是對於每一個任務都新建一個執行緒去執行。
class ThreadPerTaskExecutor implements Executor {
public void execute(Runnable r) {
new Thread(r).start();
}
}
這種方式實際上就是上面提到的One Task One New Thread的方式,這種無限建立執行緒的方法也有很多問題。
- 執行緒生命週期的開銷非常高。如果有大量任務需要執行,那麼就需要建立大量執行緒。這樣就會造成執行緒生命週期的建立和銷燬的開銷非常大。
- 資源消耗。活躍的執行緒會消耗系統資源,尤其是記憶體。如果,已經有足夠多的執行緒使所有的CPU保持忙碌狀態,那麼在建立更多的執行緒反而會降低效能。最簡單的例子是,一個4核的CPU機器,對於100個任務建立100個執行緒去執行。
- 穩定性。可建立執行緒的數量上存在一個限制。這個限制受JVM啟動引數,棧大小以及底層作業系統對執行緒的限制等因素。超過了這個限制,就可能丟擲OutOfMemoryError異常。
ExecutorService
ExecutorService介面是繼承自Executor介面,並增加了一些介面方法。介面也可以繼承?以前沒注意,現在學習到了。這裡介紹下介面繼承的語義:
- 介面Executor有execute(Runnable)方法,介面ExecutorService繼承Executor,不用複寫Executor的方法。只需要,寫自己的方法(業務)即可。
- 當一個類ThreadPoolExecutor要實現ExecutorService介面的時候,需要實現ExecutorService和Executor兩個介面的方法。
ExecutorService大致新增了2類介面方法:
- ExecutorService的關閉方法。對於執行緒池實現,這些方法的具體實現在ThreadPoolExecutor裡面。
- 擴充非同步執行任務的方法。對於執行緒池實現,用的這類方法都是AbstractExecutorService抽象類裡面實現的模板方法。
AbstractExecutorService
抽象類AbstractExecutorService提供了ExecutorService介面類中各種submit非同步執行方法的實現,這些方法與Executor.execute(Runnable)相比,它們都是有返回值的。同時,這些方法的實現的最終都是呼叫ThreadPoolExecutor類中實現的execute(Runnable)方法。
儘管說submit方法能提供執行緒執行的返回值,但只有實現了Callable才會有返回值,而實現Runnable的返回值是null。
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
除此之外,這個抽象類中還有ExecutorService介面類中invokeAny和invokeAll方法的實現。這裡就只是簡單介紹下這2個種方法的語義。
invokeAny
- invokeAny() 接收一個包含 Callable 物件的集合作為引數。呼叫該方法不會返回 Future 物件,而是返回集合中某一個Callable物件的執行結果。
- 這個方法沒法保證呼叫之後返回的結果是哪一個Callable,只知道它是這些 Callable 中一個執行結束的Callable 物件。
invokeAll
- invokeAll接受一個包含 Callable 物件的集合作為引數。呼叫該方法會返回一個Future 物件的列表,對應輸入的Callable 物件的集合的執行結果。
- 這裡提交的任務容器列表和返回的Future列表存在順序對應的關係。
ThreadPoolExecutor
execute(Runnable)方法
執行緒池是如何執行輸入的任務,這個整個執行緒池實現的核心邏輯,我們從這個方法開始學習。其程式碼如下所示:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
可以發現,當提交一個新任務到執行緒池時,執行緒池的處理流程如下:
- 判斷執行緒池中工作的執行緒數是否小於核心執行緒數(corePoolSize)。如果是,則新建一個新的工作執行緒來執行任務(需要獲取全域性鎖)。否則,進入下個流程。
- 判斷執行緒池的工作佇列(BlockingQeue)是否已滿。如果未滿,將新加的任務儲存在工作佇列中。否則,進入下個流程。
- 判斷執行緒池中工作的執行緒數是否小於最大執行緒數(maximumPoolSize)。如果小於,則新建一個工作執行緒來執行任務(需要獲取全域性鎖)。
- 如果大於或者等於,則交給飽和策略處理這個任務。
新提交任務處理流程圖
以流程圖來說明的話,執行緒池處理一個新提交的任務的流程如下圖所示:
ThreadPoolExecutor執行示意圖
從上面的內容,我們可以發現執行緒池對於一個新任務有4種處理的可能,分別對應於上面處理流程的4個步驟。
ThreadPoolExecutor採取上述步驟的總體設計思路,是為了在執行execute()方法時,儘可能地避免獲取全域性鎖(那將會是一個嚴重的可伸縮瓶頸)。在ThreadPoolExecutor完成預熱之後(當前執行的執行緒數大於等於corePoolSize),幾乎所有的execute()方法呼叫都是執行步驟2,而步驟2不需要獲取全域性鎖。
工作執行緒
從上面execute(Runnable)的程式碼我們可以發現,執行緒池建立執行緒時,會將執行緒封裝成工作執行緒Worker,Worker在執行完任務後,還會迴圈獲取工作佇列裡的任務來執行。
ThreadPoolExecutor中執行緒執行任務的示意圖如下所示:
執行緒池中的執行緒執行任務分兩種情況:
- 在execute()方法中建立一個執行緒時,會讓這個執行緒執行當前任務。
- 這個執行緒執行完上圖中1的任務後,會反覆從BlockingQueue獲取任務來執行。
ThreadPoolExecutor的ctl變數
ctl 是一個 AtomicInteger 的類,儲存的 int 變數的更新都是原子操作,保證執行緒安全。它的前面3位用來表示執行緒池狀態,後面29位用來表示工程執行緒數量。
ThreadPoolExecutor的狀態
執行緒池的狀態有5種:
-
Running:執行緒池處在Running的狀態時,能夠接收新任務,以及對已新增的任務進行處理。執行緒池的初始化狀態是RUNNING。換句話說,執行緒池被一旦被建立,就處於Running狀態,並且執行緒池中的任務數為0。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
- Shutdown: 執行緒池處在SHUTDOWN狀態時,不接收新任務,但能處理已新增(正在執行的以及在BlockingQueue)的任務。呼叫執行緒池的shutdown()介面時,執行緒池由RUNNING -> SHUTDOWN。
- Stop: 執行緒池處在STOP狀態時,不接收新任務,不處理已新增的任務,並且會中斷正在執行的任務。 呼叫執行緒池的shutdownNow()介面時,執行緒池由(RUNNING or SHUTDOWN ) -> STOP。
- Tidying: 當所有的任務已終止,ctl記錄的”任務數量”為0,執行緒池會變為Tidying狀態。當執行緒池變為Tidying狀態時,會執行鉤子函式terminated()。terminated()在ThreadPoolExecutor類中是空的,若使用者想線上程池變為Tidying時,進行相應的處理;可以通過過載terminated()函式來實現。
- Terminated: 執行緒池徹底終止,就變成Terminated狀態。 執行緒池處在Tidying狀態時,執行完terminated()之後,就會由 Tidying -> Terminated。
執行緒池的使用
執行緒池的建立
我們可以通過ThreadPoolExecutor的建構函式來建立一個執行緒池。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize(執行緒池的核心執行緒數):執行緒池要保持的執行緒數目,即使是他們是空閒也不會停止。 當提交一個任務到執行緒池時,執行緒池會建立一個執行緒來執行任務,即使其他空閒的基本執行緒能夠執行新任務也會建立執行緒,等到需要執行的任務數大於執行緒池基本大小時就不再建立。如果呼叫了執行緒池的prestartAllCoreThreads()方法,執行緒池會提前建立並啟動所有基本執行緒。
- maximumPoolSize(執行緒池的最大執行緒數): 執行緒池允許建立的最大執行緒數。如果佇列滿了,並且已建立的執行緒數小於最大執行緒數,則執行緒池會再建立新的執行緒執行任務。值得注意的是,如果使用了無界的任務佇列這個引數就沒什麼效果。
- keepAliveTime(執行緒活動保持時間): 當執行緒池中的執行緒數大於corePoolSize時,keepAliveTime為多餘的空閒執行緒等待新任務的最長保持存活的時間。所以,如果任務很多,並且每個任務執行的時間比較短,可以調大時間,提高執行緒的利用率。
- unit(執行緒活動保持時間的單位) : 可選的單位有天(DAYS)、小時(HOURS)、分鐘(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和納秒(NANOSECONDS,千分之一微秒)。
- runnableTaskQueue(任務佇列):用於儲存等待執行的任務的阻塞佇列。可以選擇以下幾個阻塞佇列。
- ArrayBlockingQueue:是一個基於陣列結構的有界阻塞佇列,此佇列按FIFO(先進先出)原則對元素進行排序。
- LinkedBlockingQueue:一個基於連結串列結構的無界阻塞佇列,此佇列按FIFO排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個佇列。
- SynchronousQueue:一個不儲存元素的阻塞佇列。每個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於Linked-BlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個佇列。
- PriorityBlockingQueue:一個具有優先順序的無限阻塞佇列。
- ThreadFactory:用於設定建立執行緒的工廠,可以通過執行緒工廠給每個建立出來的執行緒設定更有意義的名字。
-
RejectedExecutionHandler(飽和策略):當ThreadPoolExecutor已經關閉或ThreadPoolExecutor已經飽和 時(達到了最大執行緒池大小且工作佇列已滿),execute()方法將要呼叫的Handler,那麼必須採取一種策略處理提交的新任務。這個策略預設情況下是AbortPolicy。Java執行緒池框架提供了以下4種策略:
- AbortPolicy:直接丟擲異常
- CallerRunsPolicy:只用呼叫者所線上程來執行任務
- DiscardOldestPolicy:丟棄佇列裡最老的一個任務,並執行當前任務
- DiscardPolicy:不處理,丟棄掉
常用ThreadPoolExecutor
通過Executor框架的工具類Executors,可以建立以下3種型別的ThreadPoolExecutor。通過原始碼可以發現這3種執行緒池的本質都是不同輸入引數配置的ThreadPoolExecutor。
FixedThreadPool
FixedThreadPool被稱為可重用固定執行緒數的執行緒池。下面是FixedThreadPool的原始碼實現。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
注意到,
- FixedThreadPool的corePoolSize和maximumPoolSize都被設定為建立時的同一個指定的引數nThreads。
- 任務阻塞佇列使用的是無界佇列new LinkedBlockingQueue()。
- keepAliveTime設定為0。
- ThreadFactory和RejectedExecutionHandler皆使用的預設值。
FixedThreadPool的execute()方法的執行示意圖如下所示:
其執行說明:
- 如果當前執行的執行緒數少於corePoolSize,則建立新執行緒來執行任務。
- 線上程池完成預熱之後(當前執行的執行緒數等於corePoolSize),將任務加入LinkedBlockingQueue。
- 執行緒執行完1中的任務後,會在迴圈中反覆從LinkedBlockingQueue獲取任務來執行。
FixedThreadPool使用無界佇列LinkedBlockingQueue作為執行緒池的工作佇列(佇列的容量為Integer.MAX_VALUE)對執行緒池會帶來如下影響:
- 當執行緒池中的執行緒數達到corePoolSize後,新任務將在無界佇列中等待。由於無界佇列永遠不會滿,因此執行緒池中的執行緒數不會超過corePoolSize。
- 由於1,使用無界佇列時maximumPoolSize將是一個無效引數。
- 由於1和2,使用無界佇列時keepAliveTime將是一個無效引數。不會有超過corePoolSize的執行緒數目。
- 由於使用無界佇列。執行中的FixedThreadPool(未執行方法shutdown()或shutdownNow())不會拒絕任務(不會呼叫RejectedExecutionHandler.rejectedExecution方法)。
SingleThreadExecutor
SingleThreadExecutor是使用單個worker執行緒的Executor。SingleThreadExecutor與FixedThreadPool類似,只是它的corePoolSize和maximumPoolSize被設定為1。下面是SingleThreadExecutor的原始碼實現。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
CachedThreadPool
CachedThreadPool是一個會根據需要建立新執行緒的執行緒池。下面是建立CachedThread-Pool的原始碼。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
注意到:
- CachedThreadPool的corePoolSize被設定為0,即corePool為空;maximumPoolSize被設定為 Integer.MAX_VALUE,即maximumPool是無界的。
- keepAliveTime設定為60L,意味著CachedThreadPool中的空閒執行緒等待新任務的最長時間為60秒,空閒執行緒超過60秒後將會被終止。
- CachedThreadPool使用沒有容量的SynchronousQueue作為執行緒池的工作佇列,但CachedThreadPool的maximumPool是無界的。這意味著,如果主執行緒提交任務的速度高於maximumPool中執行緒處理任務的速度時,CachedThreadPool會不斷建立新執行緒。極端情況下,CachedThreadPool會因為建立過多執行緒而耗盡CPU和記憶體資源。
CacheThreadPool的execute()方法的執行過程如下圖所示:
其執行過程的說明如下:
- 首先執行SynchronousQueue.offer(Runnable task)。如果當前maximumPool中有空閒執行緒正在執行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那麼主執行緒執行offer操作與空閒執行緒執行的poll操作配對成功,主執行緒把任務交給空閒執行緒執行;否則執行下面的步驟2。
- 當初始maximumPool為空,或者maximumPool中當前沒有空閒執行緒時,將沒有執行緒執行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。這種情況下,CachedThreadPool將會建立一個新執行緒執行任務。
- 步驟2中新建立的執行緒將任務執行完後,會執行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。這個poll操作會讓空閒執行緒最多在SynchronousQueue中等待60秒鐘。如果60秒鐘內主執行緒提交了一個新任務(主執行緒執行步驟1),那麼這個空閒執行緒將執行主執行緒提交的新任務;否則,這個空閒執行緒將終止。由於空閒60秒的空閒執行緒會被終止,因此長時間保持空閒的CachedThreadPool不會使用任何資源。
向執行緒池提交任務
可以使用兩個方法向執行緒池提交任務,分別為execute()和submit()方法。
- execute()方法用於提交不需要返回值的任務,所以無法判斷任務是否被執行緒池執行成功。一般execute()方法輸入的任務是一個Runnable類的例項。
- submit()方法用於提交需要返回值的任務。執行緒池會返回一個future型別的物件,通過這個future物件可以判斷任務是否執行成功,並且可以通過future的get()方法來獲取返回值,get()方法會阻塞當前執行緒直到任務完成,而使用get(long timeout,TimeUnit unit)方法則會阻塞當前執行緒一段時間後立即返回,這時候有可能任務沒有執行完。
關閉執行緒池
可以通過呼叫執行緒池的shutdown或者shutdownNow方法來關閉執行緒池。它們的原理是遍歷執行緒池中的工作執行緒,然後逐個呼叫執行緒的interrupt方法來中斷執行緒,所以無法響應中斷的任務可能永遠無法終止。但是它們存在一定的區別。
- shutdown首先將執行緒池的狀態設定成SHUTDOWN。然後阻止新提交的任務,對於新提交的任務,如果測試到狀態不為RUNNING,則丟擲rejectedExecution 。對於已經提交(正在執行的以及在任務佇列中的)任務不會產生任何影響。同時會將那些閒置的執行緒(idleWorkers)進行中斷。
- shutdownNow首先將執行緒池的狀態設定成STOP。然後阻止新提交的任務,對於新提交的任務,如果測試到狀態不為RUNNING,則丟擲rejectedExecution 同時會中斷當前正在執行的執行緒。另外它還將BolckingQueue中的任務給移除,並將這些任務新增到列表中進行返回。
執行緒池的監控
可以通過執行緒池提供的引數進行監控,在監控執行緒池的時候可以使用以下屬性:
- taskCount:執行緒池需要執行的任務數量。
- completedTaskCount:執行緒池在執行過程中已完成的任務數量,小於或等於taskCount。
- largestPoolSize:執行緒池裡曾經建立過的最大執行緒數量。通過這個資料可以知道執行緒池是 否曾經滿過。如該數值等於執行緒池的最大大小,則表示執行緒池曾經滿過。
- getPoolSize:執行緒池的執行緒數量。如果執行緒池不銷燬的話,執行緒池裡的執行緒不會自動銷 毀,所以這個大小隻增不減。
- getActiveCount:獲取活動的執行緒數。
另外,通過擴充套件執行緒池進行監控。可以通過繼承執行緒池來自定義執行緒池,重寫執行緒池的beforeExecute、afterExecute和terminated方法,也可以在任務執行前、執行後和執行緒池關閉前執行一些程式碼來進行監控。例如,監控任務的平均執行時間、最大執行時間和最小執行時間等。這幾個方法線上程池裡是空方法。