java多線程併發包中的executors框架(線程池)和Fork/Join框架

1. 概述

1.1 Executor

是JDK1.5時引入的,引入該接口的主要目的是解耦任務本身和任務的執行。我們之前通過線程執行一個任務時,往往需要先創建一個線程,然後調用線程的start方法來執行任務。而Executor接口解耦了任務和任務的執行,該接口只有一個方法,入參爲待執行的任務

public interface Executor {
    /**
     * 執行給定的Runnable任務.
     * 根據Executor的實現不同, 具體執行方式也不相同.
    void execute(Runnable command);
}

然後有幾個實現類:

  • 同步執行任務:DirectExecutor,對於傳入的任務,只有執行完成後execute纔會返回
  • 異步執行任務:ThreadPerTaskExecutor ,對於每個任務,執行器都會創建一個新的線程去執行任務。
  • 對任務進行排隊執行: SerialExecutor,會對傳入的任務進行排隊(FIFO順序),然後從隊首取出一個任務執行

1.2 ExecutorService

Executor接口提供的功能很簡單,爲了對它進行增強,出現了ExecutorService接口,ExecutorService繼承了Executor,它在Executor的基礎上增強了對任務的控制,同時包括對自身生命週期的管理,主要有四類:

  • 關閉執行器,禁止任務的提交;
  • 監視執行器的狀態;
  • 提供對異步任務的支持;
  • 提供對批處理任務的支持。

對於Future,Future對象提供了對任務異步執行的支持,也就是說調用線程無需等待任務執行完成,提交待執行的任務後,就會立即返回往下執行。然後,可以在需要時檢查Future是否有結果了,如果任務已執行完畢,通過Future.get()方法可以獲取到執行結果——Future.get()是阻塞方法。

1.3 ScheduledExecutorService

ScheduledExecutorService提供了一系列schedule方法,可以在給定的延遲後執行提交的任務,或者每個指定的週期執行一次提交的任務,該接口繼承了ExecutorService

2. 實現類ThreadPoolExecutor

2.1 線程池作用

ThreadPoolExecutor是用來創建線程池的Executor,線程池概念與數據庫連接池類似。

當有任務需要執行時,線程池會給該任務分配線程,如果當前沒有可用線程,一般會將任務放進一個隊列中,當有線程可用時,再從隊列中取出任務並執行

線程池的引入,主要解決以下問題:

  1. 減少系統因爲頻繁創建和銷燬線程所帶來的開銷;
  2. 自動管理線程,對使用方透明,使其可以專注於任務的構建。

Executors工廠可以創建不同類型的線程池,其中有以下幾個參數:

maximumPoolSize限定了整個線程池的大小,corePoolSize限定了核心線程池的大小,corePoolSize≤maximumPoolSize(當相等時表示爲固定線程池);maximumPoolSize-corePoolSize表示非核心線程池。

2.2 線程池狀態

ThreadPoolExecutor一共定義了5種線程池狀態

  • RUNNING : 接受新任務, 且處理已經進入阻塞隊列的任務
  • SHUTDOWN : 不接受新任務, 但處理已經進入阻塞隊列的任務
  • STOP : 不接受新任務, 且不處理已經進入阻塞隊列的任務, 同時中斷正在運行的任務
  • TIDYING : 所有任務都已終止, 工作線程數爲0, 線程轉化爲TIDYING狀態並準備調用terminated方法
  • TERMINATED : terminated方法已經執行完成

各個狀態之間的流轉圖:

preview

2.3 Worker工作線程

當我們向線程池提交一個任務時,將創建一個工作線程——我們稱之爲Worker,Worker在邏輯上從屬於【核心線程池】或【非核心線程池】,具體屬於哪一種,要根據corePoolSize、maximumPoolSize、Worker總數進行判斷。ThreadPoolExecutor中只有一種類型的線程,名叫Worker,它是ThreadPoolExecutor定義的內部類,同時封裝着Runnable任務和執行該任務的Thread對象,我們稱它爲【工作線程】,它也是ThreadPoolExecutor唯一需要進行維護的線程

每個Worker對象都有一個Thread線程對象與它相對應,當任務需要執行的時候,實際是調用內部Thread對象的start方法,而Thread對象是在Worker的構造器中通過getThreadFactory().newThread(this)方法創建的,創建的Thread將Worker自身作爲任務,所以當調用Thread的start方法時,最終實際是調用了Worker.run()方法,該方法內部委託給runWorker方法執行任務

2.3.1 工作線程的創建

execute方法內部調用了addWorker方法來添加工作線程並執行任務,整個addWorker的邏輯並不複雜,分爲兩部分:
第一部分是一個自旋操作,主要是對線程池的狀態進行一些判斷,如果狀態不適合接受新任務,或者工作線程數超出了限制,則直接返回false。

第二部分才真正去創建工作線程並執行任務:首先將Runnable任務包裝成一個Worker對象,然後加入到一個工作線程集合中(名爲workers的HashSet),最後調用工作線程中的Thread對象的start方法執行任務,其實最終是委託到Worker的下面方法執行:

public void run() {
    runWorker(this);
}

2.3.2 工作線程的執行

runWoker用於執行任務,整體流程如下:

  1. while循環不斷地通過getTask()方法從隊列中獲取任務(如果工作線程自身攜帶着任務,則執行攜帶的任務);
  2. 控制執行線程的中斷狀態,保證如果線程池正在停止,則線程必須是中斷狀態,否則線程必須不是中斷狀態;
  3. 調用task.run()執行任務;
  4. 處理工作線程的退出工作。

該方法確保正在停止的線程池(STOP/TIDYING/TERMINATED)不再接受新任務,如果有新任務那麼該任務的工作線程一定是中斷狀態;確保正常狀態的線程池(RUNNING/SHUTDOWN),其所執行的任務都是不能被中斷的。

另外,getTask方法用於從任務隊列中獲取一個任務,如果獲取不到任務,會跳出while循環,最終會通過processWorkerExit方法清理工作線程。

2.3.3 工作線程的清理

processWorkerExit的作用就是將該退出的工作線程清理掉,然後看下線程池是否需要終止。processWorkerExit執行完之後,整個工作線程的生命週期也結束了,我們可以通過下圖來回顧下它的整個生命週期:

preview

 

2.4 線程池的調度流程

ExecutorService的核心方法是submit方法——用於提交一個待執行的任務.execute的執行流程可以用下圖描述

preview

execute的整個執行流程關鍵是下面兩點:

  • 如果工作線程數小於核心線程池上限(CorePoolSize),則直接新建一個工作線程並執行任務;
  • 如果工作線程數大於等於CorePoolSize,則嘗試將任務加入到隊列等待以後執行。如果加入隊列失敗了(比如隊列已滿的情況),則在總線程池未滿的情況下(CorePoolSize ≤ 工作線程數 < maximumPoolSize)新建一個工作線程立即執行任務,否則執行拒絕策略。

2.5 任務隊列

阻塞隊列就是在我們構建ThreadPoolExecutor對象時,在構造器中指定的。由於隊列是外部指定的,所以根據阻塞隊列的特性不同,Worker工作線程調用getTask方法獲取任務的執行情況也不同

1.直接提交

即直接將任務提交給等待的工作線程,這時可以選擇SynchronousQueue。因爲SynchronousQueue是沒有容量的,而且採用了無鎖算法,所以性能較好,但是每個入隊操作都要等待一個出隊操作,反之亦然。

使用SynchronousQueue時,當核心線程池滿了以後,如果不存在空閒的工作線程,則試圖把任務加入隊列將立即失敗(execute方法中使用了隊列的offer方法進行入隊操作,而SynchronousQueue在調用offer時如果沒有另一個線程等待出隊操作,則會立即返回false),因此會構造一個新的工作線程(未超出最大線程池容量時)。
由於,核心線程池是很容易滿的,所以當使用SynchronousQueue時,一般需要將maximumPoolSizes設置得比較大,否則入隊很容易失敗,最終導致執行拒絕策略,這也是爲什麼Executors工作默認提供的緩存線程池使用SynchronousQueue作爲任務隊列的原因。

2.無界任務隊列

無界任務隊列我們的選擇主要有LinkedTransferQueueLinkedBlockingQueue(近似無界,構造時不指定容量即可),從性能角度來說LinkedTransferQueue採用了無鎖算法,高併發環境下性能相對更好,但如果只是做任務隊列使用相差並不大。

使用無界隊列需要特別注意系統資源的消耗情況,因爲當核心線程池滿了以後,會首先嚐試將任務放入隊列,由於是無界隊列所以幾乎一定會成功,那麼系統瓶頸其實就是硬件了。如果任務的創建速度遠快於工作線程處理任務的速度,那麼最終會導致系統資源耗盡。Executors工廠中創建固定線程池的方法內部就是用了LinkedBlockingQueue。

3.有界任務隊列

有界任務隊列,比如ArrayBlockingQueue ,可以防止資源耗盡的情況。當核心線程池滿了以後,如果隊列也滿了,則會創建歸屬於非核心線程池的工作線程,如果非核心線程池也滿了 ,纔會執行拒絕策略。

2.6 拒絕策略

ThreadPoolExecutor在以下兩種情況下會執行拒絕策略:

  1. 當核心線程池滿了以後,如果任務隊列也滿了,首先判斷非核心線程池有沒滿,沒有滿就創建一個工作線程(歸屬非核心線程池), 否則就會執行拒絕策略;
  2. 提交任務時,ThreadPoolExecutor已經關閉了。

所謂拒絕策略,就是在構造ThreadPoolExecutor時,傳入的RejectedExecutionHandler對象

ThreadPoolExecutor一共提供了4種拒絕策略:

  • AbortPolicy(默認):拋出一個RejectedExecutionException異常
  • DiscardPolicy:無爲而治,什麼都不做,等任務自己被回收
  • DiscardOldestPolicy:丟棄任務隊列中的最近一個任務,並執行當前任務
  • CallerRunsPolicy:以自身線程來執行任務,這樣可以減緩新任務提交的速度

2.7 線程池的關閉

ExecutorService接口提供兩種方法來關閉線程池,這兩種方法的區別主要在於是否會繼續處理已經添加到任務隊列中的任務。

  • shutdown方法將線程池切換到SHUTDOWN狀態(如果已經停止,則不用切換),並調用interruptIdleWorkers方法中斷所有空閒的工作線程,最後調用tryTerminate嘗試結束線程池,注意,如果執行Runnable任務的線程本身不響應中斷,那麼也就沒有辦法終止任務。
  • shutdownNow方法的主要不同之處就是,它會將線程池的狀態至少置爲STOP,同時中斷所有工作線程(無論該線程是空閒還是運行中),同時返回任務隊列中的所有任務。

2.8 配置核心線程池的大小

  • 如果任務是 CPU 密集型(需要進行大量計算、處理,比如計算圓周率、對視頻進行高清解碼等等),則應該配置儘量少的線程,比如 CPU 個數 + 1,這樣可以避免出現每個線程都需要使用很長時間但是有太多線程爭搶資源的情況;
  • 如果任務是 IO密集型(主要時間都在 I/O,即網絡、磁盤IO,CPU 空閒時間比較多),則應該配置多一些線程,比如 CPU 數的兩倍,這樣可以更高地壓榨 CPU。

公式:最佳線程數目 = ((線程等待時間+線程CPU時間)/線程CPU時間 )* CPU數目

比如平均每個線程CPU運行時間爲0.5s,而線程等待時間(非CPU運行時間,比如IO)爲1.5s,CPU核心數爲8,那麼根據上面這個公式估算得到:((0.5+1.5)/0.5)*8=32。

3 固定線程池

創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。

ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
    int temp = i;
    newFixedThreadPool.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + ",i:" + temp);
        }
    });
}

4 單線程線程池

創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。

類似:newSingleThreadScheduledExecutor()

5 可緩存的線程池

如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。

// 1.可緩存的線程池 重複利用
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
    int temp = i;
    newCachedThreadPool.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println("threadName:" + Thread.currentThread().getName() +
                    ",i:" + temp);
        }
    });
}

6 可延時/週期調度的線程池

ScheduledThreadPoolExecutor,它是對普通線程池ThreadPoolExecutor的擴展,增加了延時調度、週期調度任務的功能。概括下ScheduledThreadPoolExecutor的主要特點:

  1. 對Runnable任務進行包裝,封裝成ScheduledFutureTask,該類任務支持任務的週期執行、延遲執行;
  2. 採用DelayedWorkQueue作爲任務隊列。該隊列是無界隊列,所以任務一定能添加成功,但是當工作線程嘗試從隊列取任務執行時,只有最先到期的任務會出隊,如果沒有任務或者隊首任務未到期,則工作線程會阻塞;
  3. ScheduledThreadPoolExecutor的任務調度流程與ThreadPoolExecutor略有區別,最大的區別就是,先往隊列添加任務,然後創建工作線程執行任務。

創建一個定長線程池,支持定時及週期性任務執行。

ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3);
for (int i = 0; i < 10; i++) {
    int temp = i;
    newScheduledThreadPool.schedule(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + ",i:" + temp);
        }
    },3, TimeUnit.SECONDS);
}

 

7. Future模式

7.1 簡介

Future模式是Java多線程設計模式中的一種常見模式,它的主要作用就是異步地執行任務,並在需要的時候獲取結果。我們知道,一般調用一個函數,需要等待函數執行完成,調用線程纔會繼續往下執行,如果是一些計算密集型任務,需要等待的時間可能就會比較長。

Future模式可以讓調用方立即返回,然後它自己會在後面慢慢處理,此時調用者拿到的僅僅是一個憑證,調用者可以先去處理其它任務,在真正需要用到調用結果的場合,再使用憑證去獲取調用結果。這個憑證就是這裏的Future

我們看下時序圖來理解下兩者的區別:

傳統的數據獲取方式:
clipboard.png

Future模式下的數據獲取:
clipboard.png

7.2 併發包中Future模式中的各個組件

7.2.1 真實的任務類

首先我們需要類可以返回線程的執行結果,而傳統實現Runnable接口的線程是獲取不了返回值的

於是,JDK提供了另一個接口——Callable,表示一個具有返回結果的任務:

public interface Callable<V> {
    V call() throws Exception;
}

所以,最終我們自定義的任務類一般都是實現了Callable接口。以下定義了一個具有複雜計算過程的任務,最終返回一個Double值:

public class ComplexTask implements Callable<Double> {
    @Override
    public Double call() {
        // complex calculating...
        return ThreadLocalRandom.current().nextDouble();
    }
}

 7.2.2 憑證

Future模式可以讓調用方獲取任務的一個憑證,以便將來拿着憑證去獲取任務結果,憑證需要具有以下特點:

  1. 在將來某個時間點,可以通過憑證獲取任務的結果;
  2. 可以支持取消。

併發包中提供了Future接口和它的實現類——FutureTask來滿足我們的需求

所以我們可以將上面的代碼改造成:

ComplexTask task = new ComplexTask();
Future<Double> future = new FutureTask<Double>(task);

上面的FutureTask就是真實的“憑證”,Future則是該憑證的接口(從面向對象的角度來講,調用方應面向接口操作)。

那既然要執行任務,FutureTask這個類其實除了實現了Future憑證接口外,還實現了Runable接口

FutureTask既可以包裝Callable任務,也可以包裝Runnable任務,但最終都是將Runnable轉換成Callable任務,其實是一個適配過程。

最終,調用方可以以下面這種方式使用Future模式,異步地獲取任務的執行結果。

public static void main(String[] args) throws ExecutionException, InterruptedException {
    ComplexTask task = new ComplexTask();
    Future<Double> future = new FutureTask<Double>(task);
    
    // time passed...
    
    Double result = future.get();
}

通過上面的分析,可以看到,整個Future模式其實就三個核心組件:

  • 真實任務/數據類(通常任務執行比較慢,或數據構造需要較長時間),即示例中的ComplexTask
  • Future接口(調用方使用該憑證獲取真實任務/數據的結果),即Future接口
  • Future實現類(用於對真實任務/數據進行包裝),即FutureTask實現類

注意:

  1. FutureTask雖然支持任務的取消(cancel方法),但是隻有當任務是初始化(NEW狀態)時纔有效,否則cancel方法直接返回false;
  2. 當執行任務時(run方法),無論成功或異常,都會先過渡到COMPLETING狀態,直到任務結果設置完成後,纔會進入響應的終態。

7.3 FutureTask

既然是任務,就有狀態,FutureTask一共給任務定義了7種狀態:

  • NEW:表示任務的初始化狀態;
  • COMPLETING:表示任務已執行完成(正常完成或異常完成),但任務結果或異常原因還未設置完成,屬於中間狀態;
  • NORMAL:表示任務已經執行完成(正常完成),且任務結果已設置完成,屬於最終狀態;
  • EXCEPTIONAL:表示任務已經執行完成(異常完成),且任務異常已設置完成,屬於最終狀態;
  • CANCELLED:表示任務還沒開始執行就被取消(非中斷方式),屬於最終狀態;
  • INTERRUPTING:表示任務還沒開始執行就被取消(中斷方式),正式被中斷前的過渡狀態,屬於中間狀態;
  • INTERRUPTED:表示任務還沒開始執行就被取消(中斷方式),且已被中斷,屬於最終狀態。

clipboard.png

7.3 結果獲取

FutureTask可以通過get方法獲取任務結果,如果需要限時等待,可以調用get(long timeout, TimeUnit unit)。如果當前任務的狀態是NEWCOMPLETING,會調用awaitDone阻塞線程。否則會認爲任務已經完成,直接通過report方法映射結果

7.4 ScheduledFutureTask

ScheduledFutureTask是ScheduledThreadPoolExecutor這個線程池的默認調度任務類。
ScheduledFutureTask在普通FutureTask的基礎上增加了週期執行/延遲執行的功能

8. Fork/Join框架

8.1 分治思想

算法領域有一種基本思想叫做“分治”,所謂“分治”就是將一個難以直接解決的大問題,分割成一些規模較小的子問題,以便各個擊破,分而治之。

比如:對於一個規模爲N的問題,若該問題可以容易地解決,則直接解決;否則將其分解爲K個規模較小的子問題,這些子問題互相獨立且與原問題性質相同,遞歸地解這些子問題,然後將各子問題的解合併得到原問題的解,這種算法設計策略叫做分治法。

許多基礎算法都運用了“分治”的思想,比如二分查找、快速排序等等。

基於“分治”的思想,J.U.C在JDK1.7時引入了一套Fork/Join框架。Fork/Join框架的基本思想就是將一個大任務分解(Fork)成一系列子任務,子任務可以繼續往下分解,當多個不同的子任務都執行完成後,可以將它們各自的結果合併(Join)成一個大結果,最終合併成大任務的結果:

圖片參考自《Java併發編程的藝術》

8.2 工作竊取算法

從上述Fork/Join框架的描述可以看出,我們需要一些線程來執行Fork出的任務,在實際中,如果每次都創建新的線程執行任務,對系統資源的開銷會很大,所以Fork/Join框架利用了線程池來調度任務。

另外,這裏可以思考一個問題,既然由線程池調度,根據我們之前學習普通/計劃線程池的經驗,必然存在兩個要素:

  • 工作線程
  • 任務隊列

一般的線程池只有一個任務隊列,但是對於Fork/Join框架來說,由於Fork出的各個子任務其實是平行關係,爲了提高效率,減少線程競爭,應該將這些平行的任務放到不同的隊列中去,如上圖中,大任務分解成三個子任務:子任務1、子任務2、子任務3,那麼就創建三個任務隊列,然後再創建3個工作線程與隊列一一對應。

由於線程處理不同任務的速度不同,這樣就可能存在某個線程先執行完了自己隊列中的任務的情況,這時爲了提升效率,我們可以讓該線程去“竊取”其它任務隊列中的任務,這就是所謂的工作竊取算法

“工作竊取”的示意圖如下,當線程1執行完自身任務隊列中的任務後,嘗試從線程2的任務隊列中“竊取”任務:

圖片參考自《Java併發編程的藝術》

對於一般的隊列來說,入隊元素都是在“隊尾”,出隊元素在“隊首”,要滿足“工作竊取”的需求,任務隊列應該支持從“隊尾”出隊元素,這樣可以減少與其它工作線程的衝突(因爲正常情況下,其它工作線程從“隊首”獲取自己任務隊列中的任務),滿足這一需求的任務隊列其實就是雙端阻塞隊列——LinkedBlockingDeque
當然,出於性能考慮,J.U.C中的Fork/Join框架並沒有直接利用LinkedBlockingDeque作爲任務隊列,而是自己重新實現了一個。 

8.3 Fork/Join組件

該框架主要涉及三大核心組件:ForkJoinPool(線程池)、ForkJoinTask(任務)、ForkJoinWorkerThread(工作線程),外加WorkQueue(任務隊列):

  • ForkJoinPool:ExecutorService的實現類,負責工作線程的管理、任務隊列的維護,以及控制整個任務調度流程;
  • ForkJoinTask:Future接口的實現類,fork是其核心方法,用於分解任務並異步執行;而join方法在任務結果計算完畢之後纔會運行,用來合併或返回計算結果;
  • ForkJoinWorkerThread:Thread的子類,作爲線程池中的工作線程(Worker)執行任務;
  • WorkQueue:任務隊列,用於保存任務;

8.3.1 ForkJoinPool

它作爲Executors框架的一員,是ExecutorService的一個實現類

ForkJoinPool的主要工作如下:

  1. 接受外部任務的提交(外部調用ForkJoinPool的invoke/execute/submit方法提交任務);
  2. 接受ForkJoinTask自身fork出的子任務的提交;
  3. 任務隊列數組(WorkQueue[])的初始化和管理;
  4. 工作線程(Worker)的創建/管理。

ForkJoinPool提供了3類外部提交任務的方法:invokeexecutesubmit,它們的主要區別在於任務的執行方式上。

  • 通過invoke方法提交的任務,調用線程直到任務執行完成纔會返回,也就是說這是一個同步方法,且有返回結果
  • 通過execute方法提交的任務,調用線程會立即返回,也就是說這是一個異步方法,且沒有返回結果
  • 通過submit方法提交的任務,調用線程會立即返回,也就是說這是一個異步方法,且有返回結果(返回Future實現類,可以通過get獲取結果)。

8.3.2 ForkJoinTask

從Fork/Join框架的描述上來看,“任務”必須要滿足一定的條件:

  1. 支持Fork,即任務自身的分解
  2. 支持Join,即任務結果的合併

ForkJoinTask就是符合這種條件的任務。

ForkJoinTask實現了Future接口,是一個異步任務,我們在使用Fork/Join框架時,一般需要使用線程池來調度任務,線程池內部調度的其實都是ForkJoinTask任務

除了ForkJoinTask,Fork/Join框架還提供了兩個ForkJoinTask的抽象實現,我們在自定義ForkJoin任務時,一般繼承這兩個類:

  • RecursiveAction:表示具有返回結果的ForkJoin任務
  • RecursiveTask:表示沒有返回結果的ForkJoin任務

其它組件就不說了

8.3.3 使用示例

假設有個非常大的long[]數組,通過FJ框架求解數組所有元素的和。 

任務類定義,因爲需要返回結果,所以繼承RecursiveTask,並覆寫compute方法。任務的fork通過ForkJoinTask的fork方法執行,join方法方法用於等待任務執行後返回.

代碼大致的意思就是:

ArraySumTask類初始化時會傳入需要計算的數組,和begin,end。通過設置的THRESHOLD 閾值來與begin,end比較.

如果end - begin + 1 < THRESHOLD,那麼不需要分段,

如果end - begin + 1 >THRESHOLD, 就需要分段計算了,怎麼分呢?就再次創建兩個ArraySumTask 任務,一個處理array的index爲0-500的數據,一個處理501-1000的數據。然後再次調用fork方法,會執行新任務的compute方法,那麼由於剛創建的兩個任務還是比閾值100大,所以分別又會創建任務,就一直遞歸創建任務,直到end-begin小於閾值。然後分別執行任務,跳出遞歸,執行join方法,將結果統一相加

public class ArraySumTask extends RecursiveTask<Long> {
 
    private final int[] array;
    private final int begin;
    private final int end;
 
    private static final int THRESHOLD = 100;
 
    public ArraySumTask(int[] array, int begin, int end) {
        this.array = array;
        this.begin = begin;
        this.end = end;
    }
 
    @Override
    protected Long compute() {
        long sum = 0;
 
        if (end - begin + 1 < THRESHOLD) {      // 小於閾值, 直接計算
            for (int i = begin; i <= end; i++) {
                sum += array[i];
            }
        } else {
            int middle = (end + begin) / 2;
            ArraySumTask subtask1 = new ArraySumTask(this.array, begin, middle);
            ArraySumTask subtask2 = new ArraySumTask(this.array, middle + 1, end);
 
            subtask1.fork();
            subtask2.fork();
 
            long sum1 = subtask1.join();
            long sum2 = subtask2.join();
 
            sum = sum1 + sum2;
        }
        return sum;
    }
}

調用方如下:

public class Main {
    public static void main(String[] args) {
        ForkJoinPool executor = new ForkJoinPool();
        ArraySumTask task = new ArraySumTask(new int[10000], 0, 9999);
 
        ForkJoinTask future = executor.submit(task);
 
        // some time passed...
 
        if (future.isCompletedAbnormally()) {
            System.out.println(future.getException());
        }
 
        try {
            System.out.println("result: " + future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
 
    }
}

注意:ForkJoinTask在執行的時候可能會拋出異常,但是沒辦法在主線程裏直接捕獲異常,所以ForkJoinTask提供了isCompletedAbnormally()方法來檢查任務是否已經拋出異常或已經被取消了,並且可以通過ForkJoinTask的getException方法獲取異常.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章