在Java中,使用線程來異步執行任務。Java線程的創建與銷毀需要一定的開銷,如果我們為每一個任務創建一個新線程來執行,這些線程的創建與銷毀將消耗大量的計算資源。同時,為每一個任務創建一個新線程來執行,這種策略可能會使處于高負荷狀態的應用最終崩潰。
Java的線程既是工作單元,也是執行機制。從JDK 5開始,把工作單元與執行機制分離開來。工作單元包括Runnable和Callable,而執行機制由Executor框架提供。
Executor框架簡介
Executor框架的兩級調度模型
在HotSpot VM的線程模型中,Java線程(java.lang.Thread)被一對一映射為本地操作系統線程。Java線程啟動時會創建一個本地操作系統線程;當該Java線程終止時,這個操作系統線程也會被回收。操作系統會調度所有線程并將它們分配給可用的CPU。
在上層,Java多線程程序通常把應用分解為若干個任務,然后使用用戶級的調度器(Executor框架)將這些任務映射為固定數量的線程;在底層,操作系統內核將這些線程映射到硬件處理器上。
這種兩級調度模型的示意圖如下:
從圖中可以看出,應用程序通過Executor框架控制上層的調度;而下層的調度由操作系統內核控制,下層的調度不受應用程序的控制。
Executor框架的結構與成員
下面將分兩部分來介紹Executor:Executor的結構和Executor框架包含的成員組件。
Executor框架的結構
Executor框架主要由3大部分組成如下:
- 任務。包括被執行任務需要實現的接口:Runnable接口或Callable接口。
-
任務的執行。包括任務執行機制的核心接口Executor,以及繼承自Executor的ExecutorService接口。Executor框架有兩個關鍵類實現了ExecutorService接口
(ThreadPoolExecutor和ScheduledThreadPoolExecutor)。 - 異步計算的結果。包括接口Future和實現Future接口的FutureTask類。
Executor框架包含的主要的類與接口如下圖所示:
下面是這些類和接口的簡介:
- Executor是一個接口,它是Executor框架的基礎,它將任務的提交與任務的執行分離開來。
- ThreadPoolExecutor是線程池的核心實現類,用來執行被提交的任務。
- ScheduledThreadPoolExecutor是一個實現類,可以在給定的延遲后運行命令,或者定期執行命令。ScheduledThreadPoolExecutor比Timer更靈活,功能更強大。
- Future接口和實現Future接口的FutureTask類,代表異步計算的結果。
- Runnable接口和Callable接口的實現類,都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor執行。
Executor框架的使用示意圖如下:
主線程首先要創建實現Runnable或者Callable接口的任務對象。工具類Executors可以把一個Runnable對象封裝為一個Callable對象(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object result))。
然后可以把Runnable對象直接交給ExecutorService執行(ExecutorService.execute(Runnable command));或者也可以把Runnable對象或Callable對象提交給ExecutorService執行(ExecutorService.submit(Runnable task)或ExecutorService.submit(Callable<T>task))。
如果執行ExecutorService.submit(…),ExecutorService將返回一個實現Future接口的對象(到目前為止的JDK中,返回的是FutureTask對象)。由于FutureTask實現了Runnable,程序員也可以創建FutureTask,然后直接交給ExecutorService執行。
最后,主線程可以執行FutureTask.get()方法來等待任務執行完成。主線程也可以執行FutureTask.cancel(boolean mayInterruptIfRunning)來取消此任務的執行。
Executor框架的成員
下面將介紹Executor框架的主要成員:ThreadPoolExecutor、ScheduledThreadPoolExecutor、Future接口、Runnable接口、Callable接口和Executors。
(1)ThreadPoolExecutor
ThreadPoolExecutor通常使用工廠類Executors來創建。Executors可以創建3種類型的ThreadPoolExecutor:SingleThreadExecutor、FixedThreadPool和CachedThreadPool。
下面分別介紹這3種ThreadPoolExecutor:
1)FixedThreadPool。下面是Executors提供的,創建使用固定線程數的FixedThreadPool的API。
FixedThreadPool適用于為了滿足資源管理的需求,而需要限制當前線程數量的應用場景,它適用于負載比較重的服務器。
2)SingleThreadExecutor。下面是Executors提供的,創建使用單個線程的SingleThreadExecutor的API。
SingleThreadExecutor適用于需要保證順序地執行各個任務;并且在任意時間點,不會有多個線程是活動的應用場景。
3)CachedThreadPool。下面是Executors提供的,創建一個會根據需要創建新線程的CachedThreadPool的API。
CachedThreadPool是大小無界的線程池,適用于執行很多的短期異步任務的小程序,或者是負載較輕的服務器。
(2)ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor通常使用工廠類Executors來創建。Executors可以創建2種類型的ScheduledThreadPoolExecutor,如下:
- ScheduledThreadPoolExecutor。包含若干個線程的ScheduledThreadPoolExecutor。
- SingleThreadScheduledExecutor。只包含一個線程的ScheduledThreadPoolExecutor。
下面分別介紹這兩種ScheduledThreadPoolExecutor。
下面是工廠類Executors提供的,創建固定個數線程的ScheduledThreadPoolExecutor的API。
ScheduledThreadPoolExecutor適用于需要多個后臺線程執行周期任務,同時為了滿足資源管理的需求而需要限制后臺線程的數量的應用場景。下面是Executors提供的,創建單個線程的SingleThreadScheduledExecutor的API。
SingleThreadScheduledExecutor適用于需要單個后臺線程執行周期任務,同時需要保證順序地執行各個任務的應用場景。
(3)Future接口
Future接口和實現Future接口的FutureTask類用來表示異步計算的結果。當我們把Runnable接口或Callable接口的實現類提交(submit)給ThreadPoolExecutor或ScheduledThreadPoolExecutor時,ThreadPoolExecutor或ScheduledThreadPoolExecutor會向我們返回一個FutureTask對象。下面是對應的API。
有一點需要讀者注意,到目前最新的JDK 8為止,Java通過上述API返回的是一個FutureTask對象。但從API可以看到,Java僅僅保證返回的是一個實現了Future接口的對象。在將來的JDK實現中,返回的可能不一定是FutureTask。
(4)Runnable接口和Callable接口
Runnable接口和Callable接口的實現類,都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor執行。它們之間的區別是Runnable不會返回結果,而Callable可以返回結果。
除了可以自己創建實現Callable接口的對象外,還可以使用工廠類Executors來把一個Runnable包裝成一個Callable。
下面是Executors提供的,把一個Runnable包裝成一個Callable的API。
下面是Executors提供的,把一個Runnable和一個待返回的結果包裝成一個Callable的API。
前面講過,當我們把一個Callable對象(比如上面的Callable1或Callable2)提交給ThreadPoolExecutor或ScheduledThreadPoolExecutor執行時,submit(…)會向我們返回一個FutureTask對象。我們可以執行FutureTask.get()方法來等待任務執行完成。當任務成功完成后FutureTask.get()將返回該任務的結果。例如,如果提交的是對象Callable1,FutureTask.get()方法將返回null;如果提交的是對象Callable2,FutureTask.get()方法將返回result對象。
ThreadPoolExecutor詳解
Executor框架最核心的類是ThreadPoolExecutor,它是線程池的實現類,主要由下列4個組
件構成。
- corePool:核心線程池的大小。
- maximumPool:最大線程池的大小。
- BlockingQueue:用來暫時保存任務的工作隊列。
- RejectedExecutionHandler:當ThreadPoolExecutor已經關閉或ThreadPoolExecutor已經飽和時(達到了最大線程池大小且工作隊列已滿),execute()方法將要調用的Handler。
通過Executor框架的工具類Executors,可以創建3種類型的ThreadPoolExecutor。
- FixedThreadPool
- SingleThreadExecutor
- CachedThreadPool
下面將分別介紹這3種ThreadPoolExecutor。
FixedThreadPool詳解
FixedThreadPool被稱為可重用固定線程數的線程池。下面是FixedThreadPool的源代碼實現。
分析:
1)FixedThreadPool的corePoolSize和maximumPoolSize都被設置為創建FixedThreadPool時指定的參數nThreads。
2)當線程池中的線程數大于corePoolSize時,keepAliveTime為多余的空閑線程等待新任務的最長時間,超過這個時間后多余的線程將被終止。這里把keepAliveTime設置為0L,意味著多余的空閑線程會被立即終止。
FixedThreadPool的execute()方法的運行示意圖如下:
說明:
1)如果當前運行的線程數少于corePoolSize,則創建新線程來執行任務。
2)在線程池完成預熱之后(當前運行的線程數等于corePoolSize),將任務加入
LinkedBlockingQueue。
3)線程執行完1中的任務后,會在循環中反復從LinkedBlockingQueue獲取任務來執行。
FixedThreadPool使用無界隊列LinkedBlockingQueue作為線程池的工作隊列(隊列的容量為Integer.MAX_VALUE)。
使用無界隊列作為工作隊列會對線程池帶來如下影響:
1)當線程池中的線程數達到corePoolSize后,新任務將在無界隊列中等待,因此線程池中的線程數不會超過corePoolSize。
2)由于1,使用無界隊列時maximumPoolSize將是一個無效參數。
3)由于1和2,使用無界隊列時keepAliveTime將是一個無效參數。
4)由于使用無界隊列,運行中的FixedThreadPool(未執行方法shutdown()或shutdownNow())不會拒絕任務(不會調用RejectedExecutionHandler.rejectedExecution方法)。
SingleThreadExecutor詳解
SingleThreadExecutor是使用單個worker線程的Executor。下面是SingleThreadExecutor的源代碼實現。
SingleThreadExecutor的corePoolSize和maximumPoolSize被設置為1。其他參數與FixedThreadPool相同。SingleThreadExecutor使用無界隊列LinkedBlockingQueue作為線程池的工作隊列(隊列的容量為Integer.MAX_VALUE)。SingleThreadExecutor使用無界隊列作為工作隊列對線程池帶來的影響與FixedThreadPool相同,這里就不贅述了。
SingleThreadExecutor的運行示意圖如下所示:
說明:
1)如果當前運行的線程數少于corePoolSize(即線程池中無運行的線程),則創建一個新線程來執行任務。
2)在線程池完成預熱之后(當前線程池中有一個運行的線程),將任務加入LinkedBlockingQueue。
3)線程執行完1中的任務后,會在一個無限循環中反復從LinkedBlockingQueue獲取任務來執行。
CachedThreadPool詳解
CachedThreadPool是一個會根據需要創建新線程的線程池。下面是創建CachedThreadPool的源代碼。
CachedThreadPool的corePoolSize被設置為0,即corePool為空;maximumPoolSize被設置為Integer.MAX_VALUE,即maximumPool是無界的。這里把keepAliveTime設置為60L,意味著CachedThreadPool中的空閑線程等待新任務的最長時間為60秒,空閑線程超過60秒后將會被終止。
FixedThreadPool和SingleThreadExecutor使用無界隊列LinkedBlockingQueue作為線程池的工作隊列。CachedThreadPool使用沒有容量的SynchronousQueue作為線程池的工作隊列,但CachedThreadPool的maximumPool是無界的。這意味著,如果主線程提交任務的速度高于maximumPool中線程處理任務的速度時,CachedThreadPool會不斷創建新線程。極端情況下,CachedThreadPool會因為創建過多線程而耗盡CPU和內存資源。
CachedThreadPool的execute()方法的執行示意圖如下所示:
說明:
1)首先執行SynchronousQueue.offer(Runnable task)。如果當前maximumPool中有空閑線程正在執行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主線程執行offer操作與空閑線程執行的poll操作配對成功,主線程把任務交給空閑線程執行,execute()方法執行完成;否則執行下面的步驟2)。
2)當初始maximumPool為空,或者maximumPool中當前沒有空閑線程時,將沒有線程執行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。這種情況下,步驟1)將失敗。此時CachedThreadPool會創建一個新線程執行任務,execute()方法執行完成。
3)在步驟2中新創建的線程將任務執行完后,會執行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。這個poll操作會讓空閑線程最多在SynchronousQueue中等待60秒鐘。如果60秒鐘內主線程提交了一個新任務(主線程執行步驟1)),那么這個空閑線程將執行主線程提交的新任務;否則,這個空閑線程將終止。由于空閑60秒的空閑線程會被終止,因此長時間保持空閑的CachedThreadPool不會使用任何資源。
前面提到過,SynchronousQueue是一個沒有容量的阻塞隊列。每個插入操作必須等待另一個線程的對應移除操作,反之亦然。CachedThreadPool使用SynchronousQueue,把主線程提交的任務傳遞給空閑線程執行。
CachedThreadPool中任務傳遞的示意圖如下所示:
ScheduledThreadPoolExecutor詳解
ScheduledThreadPoolExecutor繼承自ThreadPoolExecutor。它主要用來在給定的延遲之后運行任務,或者定期執行任務。ScheduledThreadPoolExecutor的功能與Timer類似,但ScheduledThreadPoolExecutor功能更強大、更靈活。Timer對應的是單個后臺線程,而ScheduledThreadPoolExecutor可以在構造函數中指定多個對應的后臺線程數。
ScheduledThreadPoolExecutor的運行機制
ScheduledThreadPoolExecutor的執行示意圖如下所示:
ScheduledThreadPoolExecutor為了實現周期性的執行任務,對ThreadPoolExecutor做了如下的修改:
- 使用DelayQueue作為任務隊列。
- 獲取任務的方式不同(后文會說明)。
- 執行周期任務后,增加了額外的處理(后文會說明)。
ScheduledThreadPoolExecutor的實現
前面我們提到過,ScheduledThreadPoolExecutor會把待調度的任務(ScheduledFutureTask)放到一個DelayQueue中。
ScheduledFutureTask主要包含3個成員變量,如下:
- long型成員變量time,表示這個任務將要被執行的具體時間。
- long型成員變量sequenceNumber,表示這個任務被添加到ScheduledThreadPoolExecutor中的序號。
- long型成員變量period,表示任務執行的間隔周期。
DelayQueue封裝了一個PriorityQueue,這個PriorityQueue會對隊列中的ScheduledFutureTask進行排序。排序時,time小的排在前面(時間早的任務將被先執行)。如果兩個ScheduledFutureTask的time相同,就比較sequenceNumber,sequenceNumber小的排在前面(也就是說,如果兩個任務的執行時間相同,那么先提交的任務將被先執行)。
首先,讓我們看看ScheduledThreadPoolExecutor中的線程執行周期任務的過程。
下圖ScheduledThreadPoolExecutor中的線程1執行某個周期任務的4個步驟:
說明:
1)線程1從DelayQueue中獲取已到期的ScheduledFutureTask(DelayQueue.take())。到期任務是指ScheduledFutureTask的time大于等于當前時間。
2)線程1執行這個ScheduledFutureTask。
3)線程1修改ScheduledFutureTask的time變量為下次將要被執行的時間。
4)線程1把這個修改time之后的ScheduledFutureTask放回DelayQueue中(DelayQueue.add())。
接下來,讓我們看看上面的步驟1獲取任務的過程。
下面是DelayQueue.take()方法的源代碼實現:
下面是DelayQueue.take()的執行示意圖:
如圖所示,獲取任務分為3大步驟:
1)獲取Lock。
2)獲取周期任務。
- 如果PriorityQueue為空,當前線程到Condition中等待;否則執行下面的2.2。
- 如果PriorityQueue的頭元素的time時間比當前時間大,到Condition中等待到time時間;否則執行下面的2.3。
- 獲取PriorityQueue的頭元素(2.3.1);如果PriorityQueue不為空,則喚醒在Condition中等待的所有線程(2.3.2)。
3)釋放Lock。
ScheduledThreadPoolExecutor在一個循環中執行步驟2,直到線程從PriorityQueue獲取到一個元素之后(執行2.3.1之后),才會退出無限循環(結束步驟2)。
最后,讓我們看看ScheduledThreadPoolExecutor中的線程執行任務的步驟4,把ScheduledFutureTask放入DelayQueue中的過程。
下面是DelayQueue.add()的源代碼實現:
下面是DelayQueue.add()的執行示意圖:
FutureTask詳解
在介紹 Callable 時我們知道它可以有返回值,返回值通過 Future 進行封裝。FutureTask 實現了 RunnableFuture 接口,該接口繼承自 Runnable 和 Future 接口,這使得 FutureTask 既可以當做一個任務執行,也可以有返回值。
public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V>
FutureTask簡介
FutureTask 可用于異步獲取執行結果或取消執行任務的場景。當一個計算任務需要執行很長時間,那么就可以用 FutureTask 來封裝這個任務,主線程在完成自己的任務之后再去獲取結果。
public class FutureTaskExample {
public static void main(String[] args) throws ExecutionException,
InterruptedException {
FutureTask<Integer> futureTask =
new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int result = 0;
for (int i = 0; i < 100; i++) {
Thread.sleep(10);
result += i;
}
return result;
}
});
Thread computeThread = new Thread(futureTask);
computeThread.start();
Thread otherThread = new Thread(() -> {
System.out.println("other task is running...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
otherThread.start();
System.out.println(futureTask.get());
}
}
結果
other task is running...
4950
FutureTask可以交給Executor執行,也可以由調用線程直接執行(FutureTask.run())。
根據FutureTask.run()方法被執行的時機,FutureTask可以處于下面3種狀態:
1)未啟動。FutureTask.run()方法還沒有被執行之前,FutureTask處于未啟動狀態。當創建一個FutureTask,且沒有執行FutureTask.run()方法之前,這個FutureTask處于未啟動狀態。
2)已啟動。FutureTask.run()方法被執行的過程中,FutureTask處于已啟動狀態。
3)已完成。FutureTask.run()方法執行完后正常結束,或被取消(FutureTask.cancel(…)),或執行FutureTask.run()方法時拋出異常而異常結束,FutureTask處于已完成狀態。
FutureTask的狀態遷移的示意圖如下:
當FutureTask處于未啟動或已啟動狀態時,執行FutureTask.get()方法將導致調用線程阻塞;當FutureTask處于已完成狀態時,執行FutureTask.get()方法將導致調用線程立即返回結果或拋出異常。
當FutureTask處于未啟動狀態時,執行FutureTask.cancel()方法將導致此任務永遠不會被執行;當FutureTask處于已啟動狀態時,執行FutureTask.cancel(true)方法將以中斷執行此任務線程的方式來試圖停止任務,而執行FutureTask.cancel(false)方法將不會對正在執行此任務的線程產生影響(讓正在執行的任務運行完成);當FutureTask處于已完成狀態時,執行FutureTask.cancel(…)方法將返回false。
下面是FutureTask的get方法和cancel方法的執行示意圖
FutureTask的使用
可以把FutureTask交給Executor執行;也可以通過ExecutorService.submit(…)方法返回一個FutureTask,然后執行FutureTask.get()方法或FutureTask.cancel(…)方法。除此以外,還可以單獨使用FutureTask.run()方法。
當一個線程需要等待另一個線程把某個任務執行完后它才能繼續執行,此時可以使用FutureTask。假設有多個線程執行若干任務,每個任務最多只能被執行一次。當多個線程試圖同時執行同一個任務時,只允許一個線程執行任務,其他線程需要等待這個任務執行完后才能繼續執行。下面是對應的示例代碼:
代碼執行示意圖如下:
當兩個線程試圖同時執行同一個任務時,如果Thread 1執行1.3后Thread 2執行2.1,那么接下來Thread 2將在2.2等待,直到Thread 1執行完1.4后Thread 2才能從2.2(FutureTask.get())返回。
FutureTask的實現
FutureTask的實現基于AbstractQueuedSynchronizer(以下簡稱為AQS)。java.util.concurrent中的很多可阻塞類(比如ReentrantLock)都是基于AQS來實現的。AQS是一個同步框架,它提供通用機制來原子性管理同步狀態、阻塞和喚醒線程,以及維護被阻塞線程的隊列。JDK 6中AQS被廣泛使用,基于AQS實現的同步器包括:ReentrantLock、Semaphore、ReentrantReadWriteLock、CountDownLatch和FutureTask。
每一個基于AQS實現的同步器都會包含兩種類型的操作,如下:
- 至少一個acquire操作。這個操作阻塞調用線程,除非/直到AQS的狀態允許這個線程繼續執行。FutureTask的acquire操作為get()/get(long timeout,TimeUnit unit)方法調用。
- 至少一個release操作。這個操作改變AQS的狀態,改變后的狀態可允許一個或多個阻塞線程被解除阻塞。FutureTask的release操作包括run()方法和cancel(…)方法。
基于“復合優先于繼承”的原則,FutureTask聲明了一個內部私有的繼承于AQS的子類Sync,對FutureTask所有公有方法的調用都會委托給這個內部子類。AQS被作為“模板方法模式”的基礎類提供給FutureTask的內部子類Sync,這個內部子類只需要實現狀態檢查和狀態更新的方法即可,這些方法將控制FutureTask的獲取和釋放操作。具體來說,Sync實現了AQS的tryAcquireShared(int)方法和tryReleaseShared(int)方法,Sync通過這兩個方法來檢查和更新同步狀態。
FutureTask的設計示意圖如圖所示:
如圖所示,Sync是FutureTask的內部私有類,它繼承自AQS。創建FutureTask時會創建內部私有的成員對象Sync,FutureTask所有的的公有方法都直接委托給了內部私有的Sync。
FutureTask.get()方法會調用AQS.acquireSharedInterruptibly(int arg)方法,這個方法的執行過程如下:
1)調用AQS.acquireSharedInterruptibly(int arg)方法,這個方法首先會回調在子類Sync中實現的tryAcquireShared()方法來判斷acquire操作是否可以成功。acquire操作可以成功的條件為:state為執行完成狀態RAN或已取消狀態CANCELLED,且runner不為null。
2)如果成功則get()方法立即返回。如果失敗則到線程等待隊列中去等待其他線程執行release操作。
3)當其他線程執行release操作(比如FutureTask.run()或FutureTask.cancel(…))喚醒當前線程后,當前線程再次執行tryAcquireShared()將返回正值1,當前線程將離開線程等待隊列并喚醒它的后繼線程(這里會產生級聯喚醒的效果,后面會介紹)。
4)最后返回計算的結果或拋出異常。
FutureTask.run()的執行過程如下。
1)執行在構造函數中指定的任務(Callable.call())。
2)以原子方式來更新同步狀態(調用AQS.compareAndSetState(int expect,int update),設置state為執行完成狀態RAN)。如果這個原子操作成功,就設置代表計算結果的變量result的值為Callable.call()的返回值,然后調用AQS.releaseShared(int arg)。
3)AQS.releaseShared(int arg)首先會回調在子類Sync中實現的tryReleaseShared(arg)來執行release操作(設置運行任務的線程runner為null,然會返回true);AQS.releaseShared(int arg),然后喚醒線程等待隊列中的第一個線程。
4)調用FutureTask.done()。
當執行FutureTask.get()方法時,如果FutureTask不是處于執行完成狀態RAN或已取消狀態CANCELLED,當前執行線程將到AQS的線程等待隊列中等待(見下圖的線程A、B、C和D)。當某個線程執行FutureTask.run()方法或FutureTask.cancel(...)方法時,會喚醒線程等待隊列的第一個線程(見下圖所示的線程E喚醒線程A)。
假設開始時FutureTask處于未啟動狀態或已啟動狀態,等待隊列中已經有3個線程(A、B和C)在等待。此時,線程D執行get()方法將導致線程D也到等待隊列中去等待。
當線程E執行run()方法時,會喚醒隊列中的第一個線程A。線程A被喚醒后,首先把自己從隊列中刪除,然后喚醒它的后繼線程B,最后線程A從get()方法返回。線程B、C和D重復A線程的處理流程。最終,在隊列中等待的所有線程都被級聯喚醒并從get()方法返回。