並行執行任務的Fork/Join框架

weixin_34185560發表於2017-06-22

背書中(引用書上的話):Java7中提供了用於並行執行任務的Fork/Join框架, 可以把任務分成若干個分任務,最終彙總每個分任務的結果得到總任務的結果。這篇我們來看看Fork/Join框架。

先舉個例子

一個字串陣列,需要把每個元素中的*字元的索引返回,並求和(自己編了個栗子,沒有撒實際意義),用Fork/Join框架來實現,可以定義一個處理字串陣列的總任務,然後把總任務拆分,把陣列中每個字串交給子任務去處理,然後等待子任務執行完畢,彙總結果,並返回:

package thread.ForkJoin;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;

/**
 * @Description: .
 * @Author: ZhaoWeiNan .
 * @CreatedTime: 2017/6/21 .
 * @Version: 1.0 .
 */
public class StringTask extends RecursiveTask<Integer>{
    //要處理的字串
    private String dest;

    public StringTask(String dest) {
        this.dest = dest;
    }
    //父類RecursiveTask是一個抽象類,所以需要實現compute方法
    @Override
    protected Integer compute() {
        if (dest == null || "".equals(dest))
            return 0;
        //判斷字串中 * 的索引,並返回
        return dest.indexOf("*");
    }
}

class ArrayTask extends RecursiveTask<Integer>{
    //需要處理的字串陣列
    private String[] array;

    public ArrayTask(String[] array) {
        this.array = array;
    }

    @Override
    protected Integer compute() {
        if (array == null || array.length < 1)
            return 0;

        //申明一個StringTask變數,作為子任務
        StringTask stringTask;
        //定義一個子任務佇列,用於任務執行完畢後,獲取子任務的執行結果
        List<StringTask> list = new ArrayList<>();
        int sum = 0;
        //把字串陣列的中每一個字串分給多個StringTask子任務去處理
        for (String s : array){
            //建立一個變數,作為子任務去處理字串
            stringTask = new StringTask(s);
            //執行子任務
            stringTask.fork();
            //加入子任務佇列
            list.add(stringTask);
        }

        for (StringTask task : list){
            //等子任務執行完畢,獲取子任務執行的結果,並累加
            sum += task.join();
        }

        return sum;
    }
}

class Demo{

    public static void main(String[] args){
        //初始化字串陣列
        String[] array = new String[]{"#####*####","##*########","###*#######","#*############"};
        //建立一個總任務,處理字串陣列
        ArrayTask arrayTask = new ArrayTask(array);
        //建立執行任務的執行緒池ForkJoinPool物件
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        //執行總任務
        ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(arrayTask);

        //返回任務的結果
        try {
            System.out.println(forkJoinTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

程式碼放到了開源中國:http://git.oschina.net/zhaoweinan/forkjoin,有興趣的小夥伴可以拿去
總的來說,Fork/Join框架就是一個用來並行執行任務的框架,可以把一個大任務,分成若干個子任務,等各個子任務執行完畢,可以把他們的執行結果獲取到,並匯聚,起到了並行執行任務作用。

Fork/Join框架的構成

1.ForkJoinPool

6328467-d16f7c39cb688ee2.png
ForkJoinPool類圖

ForkJoinPool繼承了AbstractExecutorService抽象類,AbstractExecutorService實現了ExecutorService介面,由此看來ForkJoinPool也是執行緒池家族的一員,


6328467-066ab107fcb1f8f8.png
過濾了下方法,只顯示了公共方法,並擷取了一下

ForkJoinPool使用invoke、execute、submit用來執行任務。

2.ForkJoinTask

6328467-41f95d6b5da9853c.png
ForkJoinTask類圖

ForkJoinTask是Fork/Join框架使用的任務類,實現了Future介面,我們一般使用它的兩個子類RecursiveTask和RecursiveAction,


6328467-13d9ec4d9c74eed5.png
RecursiveAction類圖
public abstract class RecursiveAction extends ForkJoinTask<Void> {
    private static final long serialVersionUID = 5232453952276485070L;

RecursiveAction適用於沒有返回結果的任務,

6328467-d331600a8ee715d6.png
RecursiveTask類圖
public abstract class RecursiveTask<V> extends ForkJoinTask<V> {
    private static final long serialVersionUID = 5232453952276485270L;

RecursiveTask適用於有返回值的任務。

Work-Stealing (工作竊取)

粗略說一下Work-Stealing,ForkJoinPool具有 Work-Stealing (工作竊取)的能力,什麼意思呢?就拿文章開頭的栗子來說,把處理字串陣列的大任務,分成了若干個處理字串的子任務,這些子任務執行緒執行完畢後,不會閒著,回去執行別的子任務,通俗的來說,Work-Stealing (工作竊取)就是執行緒從其他佇列裡面獲取任務來執行。

Work-Stealing的優點

充分利用了執行緒,提高了執行緒並行執行任務的效率,並減少了執行緒間競爭帶來的系統開銷。

Work-Stealing的缺點

存在競爭的情況,而且佔用了更多的系統資源。

Fork/Join框架原理

ForkJoinPool分析

貼一張ForkJoinPool的類圖


6328467-08f8e31a13acd939.png
大小不好調整,就擷取一般吧

注意箭頭所指的兩個屬性,
ForkJoinTask<?>陣列submissionQueue,存放程式加到ForkJoinPool的任務

    private ForkJoinTask<?>[] submissionQueue;

ForkJoinWorkerThread類繼承了Thread,是一個執行緒類, ForkJoinWorkerThread[] workers就是一個執行緒陣列,負責去執行submissionQueue中的任務

    ForkJoinWorkerThread[] workers;

    .....
    public class ForkJoinWorkerThread extends Thread

ForkJoinTask分析

fork方法

獲取當前ForkJoinWorkerThread執行緒,呼叫ForkJoinWorkerThread的pushTask方法執行ForkJoinTask任務

   public final ForkJoinTask<V> fork() {
        //獲取當前ForkJoinWorkerThread執行緒,呼叫ForkJoinWorkerThread的pushTask方法執行任務
        ((ForkJoinWorkerThread) Thread.currentThread())
                .pushTask(this);
        return this;
    }

再來看看ForkJoinWorkerThread的pushTask方法:

final void pushTask(ForkJoinTask<?> t) {
        ForkJoinTask<?>[] q; int s, m;
        if ((q = queue) != null) {    // ignore if queue removed
            long u = (((s = queueTop) & (m = q.length - 1)) << ASHIFT) + ABASE;
            UNSAFE.putOrderedObject(q, u, t);
            queueTop = s + 1;         // or use putOrderedInt
            if ((s -= queueBase) <= 2)
                //呼叫執行緒池ForkJoinPool的signalWork方法
                pool.signalWork();
            else if (s == m)
                growQueue();
        }
    }

ForkJoinWorkerThread的pushTask方法把任務ForkJoinTask加到了ForkJoinTask[]任務陣列中,並呼叫了ForkJoinPool執行緒池的signalWork方法喚醒執行緒或者建立一個執行緒去執行任務,粗略的貼一下signalWork的關鍵程式碼:

private void addWorker() {
        Throwable ex = null;
        ForkJoinWorkerThread t = null;
        try {
            t = factory.newThread(this);
        } catch (Throwable e) {
            ex = e;
        }
        if (t == null) {  // null or exceptional factory return
            long c;       // adjust counts
            do {} while (!UNSAFE.compareAndSwapLong
                         (this, ctlOffset, c = ctl,
                          (((c - AC_UNIT) & AC_MASK) |
                           ((c - TC_UNIT) & TC_MASK) |
                           (c & ~(AC_MASK|TC_MASK)))));
            // Propagate exception if originating from an external caller
            if (!tryTerminate(false) && ex != null &&
                !(Thread.currentThread() instanceof ForkJoinWorkerThread))
                UNSAFE.throwException(ex);
        }
        else
            t.start();
    }

最終呼叫到了這裡,來執行任務。

join方法

從文章開頭的栗子來看,join方法會阻塞當前執行緒,等待獲取任務執行的結果

    //百度了這四種狀態的含義
    private static final int NORMAL      = -1;   //NORMAL已完成
    private static final int CANCELLED   = -2;  //CANCELLED已取消
    private static final int EXCEPTIONAL = -3;  //EXCEPTIONAL出現異常
    private static final int SIGNAL      =  1;  //SIGNAL訊號

     public final V join() {
        //先呼叫doJoin方法判斷上面定義的四個狀態
        if (doJoin() != NORMAL)
            return reportResult();
        else
            return getRawResult();
    }

join方法先呼叫doJoin方法判斷任務的狀態,看看doJoin方法,

   private int doJoin() {
        Thread t; ForkJoinWorkerThread w; int s; boolean completed;
        //獲取當前ForkJoinWorkerThread執行緒
        if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) {
            //如果狀態是小於0 也就是 -1,-2,-3 分別代表已完成、已取消、出現異常
            //直接返回狀態
            if ((s = status) < 0)
                return s;
            if ((w = (ForkJoinWorkerThread)t).unpushTask(this)) {
                //如果狀態為1|SIGNAL|訊號
                //執行任務
                try {
                    completed = exec();
                } catch (Throwable rex) {
                    //出現異常,把狀態改為-3|EXCEPTIONAL|出現異常,返回
                    return setExceptionalCompletion(rex);
                }
                if (completed)
                    //執行成功,把狀態改為-1|NORMAL|已完成,返回
                    return setCompletion(NORMAL);
            }
            return w.joinTask(this);
        }
        else
            return externalAwaitDone();
    }

doJoin檢視任務的狀態,如果狀態是-1|NORMAL|已完成,-2|CANCELLED|已取消,-3|EXCEPTIONAL|出現異常,證明任務已經執行完畢,返回狀態位,如果狀態是 1|SIGNAL|訊號,則去執行任務,如果執行成功返回-1|NORMAL|已完成,出現異常返回-3|EXCEPTIONAL|出現異常。
再來看看返回結果的reportResult方法和getRawResult方法:

private V reportResult() {
        int s; Throwable ex;
        //如果狀態為-2|CANCELLED|已取消,丟擲一個CancellationException異常
        if ((s = status) == CANCELLED)
            throw new CancellationException();
        if (s == EXCEPTIONAL && (ex = getThrowableException()) != null)
            UNSAFE.throwException(ex);
        //呼叫getRawResult方法返回結果
        return getRawResult();
    }

reportResult方法,先會判斷狀態,如果狀態為-2|CANCELLED|已取消,則丟擲一個CancellationException異常,否則呼叫getRawResult方法返回結果:

    public abstract V getRawResult();

getRawResult方法在ForkJoinTask類是抽象方法,具體實現在他的兩子類中。
RecursiveAction子類:

    public final Void getRawResult() { return null; }

所以說RecursiveAction子類使用於沒有返回值的任務。
RecursiveTask子類:

public final V getRawResult() {
        return result;
    }

RecursiveTask子類適用於有返回值的任務。

並行執行任務的Fork/Join框架是說完了。
歡迎大家來交流,指出文中一些說錯的地方,讓我加深認識。
謝謝大家!

相關文章