更多 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ù)。
-
-
ForkJoinPool
:ForkJoinTask
需要通過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)原理
ForkJoinPool
由 ForkJoinTask
數(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)用 ForkJoinTask
的 fork
方法時,程序會調(diào)用 ForkJoinWorkerThread
的 push
方法異步的執(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)用 ForkJoinPool
的 signalWork()
方法喚醒或創(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)了 Executor
和 ExecutorService
接口。
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