Java Fork/Join框架 學(xué)習(xí)筆記

更多 Java 并發(fā)編程方面的文章,請參見文集《Java 并發(fā)編程》


什么是 Fork/Join 框架

Fork/Join 框架是 Java7 提供了的一個用于并行執(zhí)行任務(wù)的框架, 是一個把大任務(wù)分割成若干個小任務(wù),最終匯總每個小任務(wù)結(jié)果后得到大任務(wù)結(jié)果的框架。

  • Fork 就是把一個大任務(wù)切分為若干子任務(wù)并行的執(zhí)行
  • Join 就是合并這些子任務(wù)的執(zhí)行結(jié)果,最后得到這個大任務(wù)的結(jié)果。

比如計算 1+2+...+10000,可以分割成若干個子任務(wù),每個子任務(wù)分別對 10 個數(shù)進行求和,最終匯總這若干個子任務(wù)的結(jié)果。

工作竊取算法

fork-join 最核心的地方就是利用了現(xiàn)代硬件設(shè)備多核,在一個操作時候會有空閑的 CPU,那么如何利用好這個空閑的 CPU 就成了提高性能的關(guān)鍵。

fork-join 框架通過一種稱作工作竊取(work stealing) 的技術(shù)減少了工作隊列的爭用情況。
每個工作線程都有自己的工作隊列,這是使用雙端隊列(或者叫做 deque)來實現(xiàn)的。當(dāng)一個任務(wù)劃分一個新線程時,它將自己推到 deque 的頭部。當(dāng)一個任務(wù)執(zhí)行與另一個未完成任務(wù)的合并操作時,它會將另一個任務(wù)推到隊列頭部并執(zhí)行,而不會休眠以等待另一任務(wù)完成。當(dāng)線程的任務(wù)隊列為空,它將嘗試從另一個線程的 deque 的尾部 竊取另一個任務(wù)。

可以使用標(biāo)準(zhǔn)隊列實現(xiàn)工作竊取,但是與標(biāo)準(zhǔn)隊列相比,deque 具有兩方面的優(yōu)勢:減少爭用和竊取。
因為只有工作線程會訪問自身的 deque 的頭部,deque 頭部永遠不會發(fā)生爭用;因為只有當(dāng)一個線程空閑時才會訪問 deque 的尾部,所以也很少存在線程的 deque 尾部的爭用。

Fork/Join 框架的介紹

  • 第一步分割任務(wù)。首先我們需要有一個 fork 類來把大任務(wù)分割成子任務(wù),有可能子任務(wù)還是很大,所以還需要不停的分割,直到分割出的子任務(wù)足夠小。
  • 第二步執(zhí)行任務(wù)并合并結(jié)果。分割的子任務(wù)分別放在雙端隊列里,然后幾個啟動線程分別從雙端隊列里獲取任務(wù)執(zhí)行。子任務(wù)執(zhí)行完的結(jié)果都統(tǒng)一放在一個隊列里,啟動一個線程從隊列里拿數(shù)據(jù),然后合并這些數(shù)據(jù)。

Fork/Join 使用兩個類來完成以上兩件事情:

  • ForkJoinTask:我們要使用 ForkJoin 框架,必須首先創(chuàng)建一個 ForkJoin 任務(wù)。它提供在任務(wù)中執(zhí)行 fork()join() 操作的機制,通常情況下我們不需要直接繼承 ForkJoinTask 類,而只需要繼承它的子類,F(xiàn)ork/Join 框架提供了以下兩個子類:
    • RecursiveAction:用于沒有返回結(jié)果的任務(wù)。
    • RecursiveTask:用于有返回結(jié)果的任務(wù)。
  • ForkJoinPoolForkJoinTask 需要通過 ForkJoinPool 來執(zhí)行,任務(wù)分割出的子任務(wù)會添加到當(dāng)前工作線程所維護的雙端隊列中,進入隊列的頭部。當(dāng)一個工作線程的隊列里暫時沒有任務(wù)時,它會隨機從其他工作線程的隊列的尾部獲取一個任務(wù)。

示例:計算 1+2+...+10000

public class SumTask extends RecursiveTask<Integer> {
    private static int THRESHOLD = 10;
    private int start;
    private int end;

    public SumTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;

        // 如果任務(wù)足夠小就計算任務(wù)
        if (end - start <= THRESHOLD) {
            for (int i = start; i <= end; i++) {
                sum = sum + i;
            }
        }
        // 否則,分割成2個子任務(wù)的計算
        else {
            int middle = (start + end) / 2;

            SumTask left = new SumTask(start, middle);
            SumTask right = new SumTask(middle + 1, end);

            // 執(zhí)行子任務(wù)
            left.fork();
            right.fork();

            // 等待子任務(wù)執(zhí)行結(jié)束,獲得結(jié)果
            int leftResult = left.join();
            int rightResult = right.join();

            // 合并子任務(wù)的結(jié)果
            sum = leftResult + rightResult;
        }

        return sum;
    }

    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();

        SumTask task = new SumTask(1, 10000);

        Future<Integer> future = pool.submit(task);

        try {
            System.out.println(future.get());
        } catch (Exception e) {
        }
    }
}

Fork/Join框架的實現(xiàn)原理

ForkJoinPoolForkJoinTask 數(shù)組和 ForkJoinWorkerThread 數(shù)組組成:

  • ForkJoinTask數(shù)組負責(zé)存放程序提交給 ForkJoinPool的任務(wù)
    • public abstract class ForkJoinTask<V> implements Future<V>, Serializable
  • ForkJoinWorkerThread數(shù)組負責(zé)執(zhí)行這些任務(wù)

當(dāng)我們調(diào)用 ForkJoinTaskfork 方法時,程序會調(diào)用 ForkJoinWorkerThreadpush 方法異步的執(zhí)行這個任務(wù),然后立即返回結(jié)果。

public final ForkJoinTask<V> fork() {
    Thread t;
    if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
        ((ForkJoinWorkerThread)t).workQueue.push(this);
    else
        ForkJoinPool.common.externalPush(this);
    return this;
}

push 方法把當(dāng)前任務(wù)存放在 ForkJoinTask 數(shù)組 queue 里。然后再調(diào)用 ForkJoinPoolsignalWork()
方法喚醒或創(chuàng)建一個工作線程來執(zhí)行任務(wù)。

final void push(ForkJoinTask<?> task) {
    ForkJoinTask<?>[] a; ForkJoinPool p;
    int b = base, s = top, n;
    if ((a = array) != null) {    // ignore if queue removed
        int m = a.length - 1;     // fenced write for task visibility
        U.putOrderedObject(a, ((m & s) << ASHIFT) + ABASE, task);
        U.putOrderedInt(this, QTOP, s + 1);
        if ((n = s - b) <= 1) {
            if ((p = pool) != null)
                p.signalWork(p.workQueues, this);
        }
        else if (n >= m)
            growArray();
    }
}

Fork/Join VS ThreadPoolExecutor

ForkJoin 同 ThreadPoolExecutor 一樣,也實現(xiàn)了 ExecutorExecutorService 接口。
public class ForkJoinPool extends AbstractExecutorService
它使用了一個無限隊列來保存需要執(zhí)行的任務(wù),而線程的數(shù)量則是通過構(gòu)造函數(shù)傳入,如果沒有向構(gòu)造函數(shù)中傳入希望的線程數(shù)量,那么當(dāng)前計算機可用的 CPU 數(shù)量會被設(shè)置為線程數(shù)量作為默認值。

ForkJoinPool 主要用來使用分治法(Divide-and-Conquer Algorithm)來解決問題。
當(dāng)使用 ThreadPoolExecutor 時,使用分治法會存在問題,因為 ThreadPoolExecutor 中的線程無法向任務(wù)隊列中再添加一個任務(wù)并且在等待該任務(wù)完成之后再繼續(xù)執(zhí)行。而使用 ForkJoinPool 時,就能夠讓其中的線程創(chuàng)建新的任務(wù),并掛起當(dāng)前的任務(wù),此時線程就能夠從隊列中選擇子任務(wù)執(zhí)行。

使用 ForkJoinPool 能夠使用數(shù)量有限的線程來完成非常多的具有父子關(guān)系的任務(wù),比如使用 4 個線程來完成超過 200 萬個任務(wù)。但是,使用 ThreadPoolExecutor 時,是不可能完成的,因為 ThreadPoolExecutor中的 Thread 無法選擇優(yōu)先執(zhí)行子任務(wù),需要完成200萬個具有父子關(guān)系的任務(wù)時,也需要200萬個線程,顯然這是不可行的。


引用:
聊聊并發(fā)(八)——Fork/Join框架介紹
應(yīng)用 fork-join 框架
深入淺出parallelStream

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。