Java ForkJoin 框架全面解析:分而治之的并行編程藝術(shù)
Java Fork/Join 框架是一種用于并行執(zhí)行任務(wù)的框架,它基于分而治之(divide-and-conquer)策略。這種策略將一個(gè)大任務(wù)分割成多個(gè)小任務(wù),然后將這些小任務(wù)并行執(zhí)行,最后合并這些小任務(wù)的執(zhí)行結(jié)果以得到最終結(jié)果。Fork/Join 框架在 Java 7 中被引入,并被設(shè)計(jì)為在 Java 8 中進(jìn)一步增強(qiáng)和優(yōu)化。

課程導(dǎo)言
適用對(duì)象
本課程適合已經(jīng)掌握J(rèn)ava多線程基礎(chǔ)(如Thread、Runnable、synchronized),并初步了解JUC并發(fā)工具(如線程池、ConcurrentHashMap)的開發(fā)者。ForkJoin框架是Java并發(fā)編程的高級(jí)主題,理解它將幫助你掌握"分而治之"的并行計(jì)算思想,為后續(xù)學(xué)習(xí)大數(shù)據(jù)處理框架(如MapReduce)打下堅(jiān)實(shí)基礎(chǔ)。
學(xué)習(xí)目標(biāo)
通過本文的系統(tǒng)學(xué)習(xí),你將能夠:
- 理解 ForkJoin框架的核心思想:分治法與工作竊取算法
- 掌握 ForkJoinPool、ForkJoinTask、RecursiveTask/RecursiveAction的核心API
- 熟練使用 ForkJoin框架解決可分解的并行計(jì)算問題
- 學(xué)會(huì) 任務(wù)粒度的選擇、性能調(diào)優(yōu)和常見陷阱規(guī)避
- 了解 ForkJoin在現(xiàn)代Java并發(fā)生態(tài)中的應(yīng)用(如Parallel Stream)
為什么需要ForkJoin?
在并發(fā)編程中,我們經(jīng)常遇到一些可以"分而治之"的大任務(wù):比如遍歷超大數(shù)組求和、處理海量文件、計(jì)算復(fù)雜遞歸函數(shù)(如斐波那契數(shù)列)、并行排序等。將這些大任務(wù)拆分成小任務(wù)并行執(zhí)行,最后合并結(jié)果,往往能獲得巨大的性能提升。
傳統(tǒng)的ThreadPoolExecutor雖然也能處理多任務(wù),但它面臨一個(gè)核心挑戰(zhàn):當(dāng)任務(wù)之間存在父子依賴關(guān)系時(shí),如何高效調(diào)度? 例如,一個(gè)任務(wù)分解成兩個(gè)子任務(wù),這兩個(gè)子任務(wù)完成后才能繼續(xù)父任務(wù)。如果用傳統(tǒng)線程池,你需要手動(dòng)管理這些依賴,代碼復(fù)雜且容易出錯(cuò)。
ForkJoin框架正是JDK為這種場景提供的專門解決方案。它由并發(fā)大師Doug Lea設(shè)計(jì),自JDK 7引入,是java.util.concurrent包中最精巧、最高效的組件之一。
第一部分:核心思想——分治法 + 工作竊取
1.1 分治法:從大化小,逐個(gè)擊破
分治法(Divide-and-Conquer)是一種古老的算法思想,其核心可以用十二個(gè)字概括:分解、解決、合并。
- 分解(Fork):將一個(gè)大的任務(wù)遞歸地拆分成若干個(gè)規(guī)模更小的子任務(wù),直到子任務(wù)簡單到可以直接計(jì)算(達(dá)到設(shè)定的閾值)。
- 解決:并行執(zhí)行這些子任務(wù)。
- 合并(Join):等待所有子任務(wù)完成,并將它們的結(jié)果按順序合并,得到最終結(jié)果。
這種思想天然適合并行處理。典型的應(yīng)用包括歸并排序、快速排序、大數(shù)求和、矩陣運(yùn)算等。
1.2 工作竊?。鹤詣?dòng)負(fù)載均衡的靈魂
工作竊?。╓ork-Stealing)算法是ForkJoin框架性能卓越的核心所在。它解決了傳統(tǒng)線程池中線程負(fù)載不均的問題。
為什么需要工作竊???
假設(shè)我們將一個(gè)大任務(wù)拆分成10個(gè)小任務(wù),并啟動(dòng)了5個(gè)線程。由于每個(gè)任務(wù)的執(zhí)行時(shí)間可能不同,有的線程很快完成了自己的任務(wù),有的線程還在忙。如果空閑線程只是等待,就浪費(fèi)了寶貴的CPU資源。工作竊取正是讓空閑線程主動(dòng)去幫助繁忙線程的一種機(jī)制。
工作竊取的實(shí)現(xiàn)原理
- 每個(gè)線程有自己的雙端隊(duì)列:在ForkJoinPool中,每個(gè)工作線程(
ForkJoinWorkerThread)都維護(hù)著一個(gè)雙端隊(duì)列(Deque),用于存放分配給它的任務(wù)。 - 線程從隊(duì)列頭部獲取任務(wù):當(dāng)線程執(zhí)行自己的任務(wù)時(shí),采用**LIFO(后進(jìn)先出)**的順序從隊(duì)列頭部取出任務(wù)執(zhí)行。LIFO策略的優(yōu)勢(shì)在于:最近被推入的任務(wù)通常是最新拆分的子任務(wù),其相關(guān)數(shù)據(jù)很可能還在CPU緩存中,執(zhí)行效率更高。
- 竊取線程從隊(duì)列尾部偷任務(wù):當(dāng)一個(gè)線程的任務(wù)隊(duì)列為空時(shí),它不會(huì)閑著,而是隨機(jī)選擇一個(gè)其他線程的隊(duì)列,從該隊(duì)列的尾部竊取一個(gè)任務(wù)來執(zhí)行。竊取時(shí)采用**FIFO(先進(jìn)先出)**的順序。
- 雙端隊(duì)列減少競爭:這種設(shè)計(jì)巧妙地減少了競爭。因?yàn)楸桓`取線程(頭部操作)和竊取線程(尾部操作)通常操作隊(duì)列的不同端,只有在隊(duì)列中只剩一個(gè)任務(wù)時(shí)才會(huì)發(fā)生競爭,但這種情況的概率較低。
工作竊取的優(yōu)點(diǎn):
- 自動(dòng)負(fù)載均衡:空閑線程自動(dòng)幫助繁忙線程,充分利用所有CPU核心。
- 減少競爭:雙端隊(duì)列設(shè)計(jì)使大部分操作無鎖化。
- 高效緩存利用:LIFO處理方式提高了緩存命中率。
缺點(diǎn):在某些極端情況下(如隊(duì)列只剩一個(gè)任務(wù)),仍存在競爭;同時(shí)維護(hù)多個(gè)雙端隊(duì)列也增加了系統(tǒng)開銷。
第二部分:ForkJoin框架核心組件
ForkJoin框架主要由三個(gè)核心組件構(gòu)成:
2.1 ForkJoinPool —— 任務(wù)調(diào)度器
ForkJoinPool是ForkJoin框架的線程池實(shí)現(xiàn),它繼承了AbstractExecutorService,因此也是一個(gè)特殊的ExecutorService。但與ThreadPoolExecutor不同,它的內(nèi)部不是用一個(gè)共享的任務(wù)隊(duì)列,而是維護(hù)了一個(gè)工作隊(duì)列數(shù)組(WorkQueue[]),每個(gè)工作隊(duì)列對(duì)應(yīng)一個(gè)工作線程。
創(chuàng)建ForkJoinPool
// 方式一:使用默認(rèn)構(gòu)造器(并行度 = CPU核心數(shù)) ForkJoinPool pool1 = new ForkJoinPool(); // 方式二:指定并行度 ForkJoinPool pool2 = new ForkJoinPool(4); // 使用4個(gè)線程 // 方式三:使用公共池(推薦!) ForkJoinPool commonPool = ForkJoinPool.commonPool();
關(guān)于公共池(commonPool):從JDK 8開始,ForkJoinPool提供了一個(gè)靜態(tài)的commonPool()方法,返回一個(gè)全局共享的線程池實(shí)例。官方強(qiáng)烈推薦大多數(shù)應(yīng)用程序使用這個(gè)公共池,因?yàn)樗梢怨?jié)省資源,讓多個(gè)ForkJoin任務(wù)共享同一個(gè)線程池,避免創(chuàng)建大量線程。公共池的線程在空閑時(shí)會(huì)慢慢回收,需要時(shí)再重新創(chuàng)建。Parallel Stream底層使用的正是這個(gè)公共池。
核心方法
| 方法 | 描述 |
|---|---|
execute(ForkJoinTask) | 異步執(zhí)行任務(wù),無返回值 |
submit(ForkJoinTask) | 異步執(zhí)行任務(wù),返回Future對(duì)象 |
invoke(ForkJoinTask) | 同步執(zhí)行任務(wù),等待任務(wù)完成并返回結(jié)果 |
invokeAll(ForkJoinTask...) | 批量提交多個(gè)子任務(wù),等待所有完成 |
2.2 ForkJoinTask —— 任務(wù)的抽象
ForkJoinTask是提交給ForkJoinPool執(zhí)行的任務(wù)的基類。它提供了fork()、join()等核心方法,并實(shí)現(xiàn)了Future接口。在實(shí)際開發(fā)中,我們幾乎從不直接繼承ForkJoinTask,而是繼承它的兩個(gè)抽象子類:
RecursiveTask —— 有返回值的任務(wù)
適用于需要返回計(jì)算結(jié)果的任務(wù),如數(shù)組求和、斐波那契數(shù)列計(jì)算。
核心方法:protected abstract V compute(),你需要在這個(gè)方法中實(shí)現(xiàn)任務(wù)的分解和計(jì)算邏輯。
RecursiveAction —— 無返回值的任務(wù)
適用于只需要執(zhí)行動(dòng)作而不需要返回結(jié)果的任務(wù),如遍歷目錄、數(shù)組元素批量修改。
核心方法:protected abstract void compute()。
fork() 與 join() 的奧秘
- fork():異步執(zhí)行當(dāng)前任務(wù)。它并不是簡單地啟動(dòng)一個(gè)新線程,而是將當(dāng)前任務(wù)推入當(dāng)前工作線程的工作隊(duì)列(如果當(dāng)前線程是
ForkJoinWorkerThread),或者推入ForkJoinPool的提交隊(duì)列。fork()方法會(huì)立即返回,不會(huì)阻塞。 - join():等待任務(wù)執(zhí)行完成并獲取結(jié)果。如果任務(wù)尚未完成,join()會(huì)阻塞當(dāng)前線程,直到任務(wù)完成。在阻塞期間,如果當(dāng)前線程是工作線程,它不會(huì)閑著,而是會(huì)嘗試竊取并執(zhí)行其他任務(wù),以提高CPU利用率——這是ForkJoin框架設(shè)計(jì)的精妙之處。
關(guān)鍵理解:fork()和join()的配對(duì)使用,加上工作竊取機(jī)制,使得ForkJoin框架能夠用少量線程高效處理大量有依賴關(guān)系的任務(wù)。
2.3 ForkJoinWorkerThread —— 執(zhí)行任務(wù)的工作線程
這是執(zhí)行ForkJoinTask的線程。每個(gè)工作線程都關(guān)聯(lián)著一個(gè)自己的雙端隊(duì)列,用于存放它fork出來的子任務(wù)。工作線程的生命周期由ForkJoinPool統(tǒng)一管理。
第三部分:實(shí)戰(zhàn)案例——從入門到精通
理論知識(shí)講完了,現(xiàn)在通過三個(gè)由淺入深的實(shí)戰(zhàn)案例,帶你掌握ForkJoin的具體用法。
3.1 案例一:數(shù)組求和(RecursiveTask入門)
這是ForkJoin最經(jīng)典的入門案例。我們計(jì)算一個(gè)超大數(shù)組中所有元素的和。
代碼實(shí)現(xiàn)
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
/**
* 使用Fork/Join計(jì)算數(shù)組求和
*/
public class ArraySumCalculator extends RecursiveTask<Long> {
private final int[] array;
private final int start;
private final int end;
private static final int THRESHOLD = 10000; // 閾值:當(dāng)數(shù)組長度小于此值時(shí),不再拆分
public ArraySumCalculator(int[] array) {
this(array, 0, array.length);
}
private ArraySumCalculator(int[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
int length = end - start;
// 1. 如果任務(wù)足夠小,直接計(jì)算(不再分解)
if (length <= THRESHOLD) {
return computeDirectly();
}
// 2. 任務(wù)拆分
int mid = start + length / 2;
ArraySumCalculator leftTask = new ArraySumCalculator(array, start, mid);
ArraySumCalculator rightTask = new ArraySumCalculator(array, mid, end);
// 3. 異步執(zhí)行左半部分任務(wù)(fork)
leftTask.fork();
// 4. 當(dāng)前線程繼續(xù)執(zhí)行右半部分(同步執(zhí)行)
Long rightResult = rightTask.compute();
// 5. 等待左半部分結(jié)果(join)
Long leftResult = leftTask.join();
// 6. 合并結(jié)果
return leftResult + rightResult;
}
private long computeDirectly() {
long sum = 0;
for (int i = start; i < end; i++) {
sum += array[i];
}
return sum;
}
public static void main(String[] args) {
// 創(chuàng)建測(cè)試數(shù)組:1到10000000
int[] array = new int[10_000_000];
for (int i = 0; i < array.length; i++) {
array[i] = i + 1;
}
// 使用ForkJoin計(jì)算
ForkJoinPool pool = new ForkJoinPool();
ArraySumCalculator task = new ArraySumCalculator(array);
long startTime = System.currentTimeMillis();
Long result = pool.invoke(task);
long endTime = System.currentTimeMillis();
System.out.println("計(jì)算結(jié)果: " + result);
System.out.println("耗時(shí): " + (endTime - startTime) + "ms");
// 驗(yàn)證結(jié)果(數(shù)學(xué)公式:n(n+1)/2)
long expected = (long) array.length * (array.length + 1) / 2;
System.out.println("結(jié)果正確: " + result.equals(expected));
pool.shutdown();
}
}代碼詳解
- 閾值(THRESHOLD):決定何時(shí)停止拆分。過小會(huì)導(dǎo)致任務(wù)拆分過細(xì),增加調(diào)度開銷;過大會(huì)導(dǎo)致并行度不足。需要根據(jù)實(shí)際情況調(diào)整。
- compute()方法:核心邏輯。先判斷任務(wù)是否足夠小,是則直接計(jì)算;否則拆分成左右兩個(gè)子任務(wù)。
- fork()與compute()的配合:這里我們
fork()了左任務(wù),而右任務(wù)由當(dāng)前線程同步執(zhí)行。這是一種常見的優(yōu)化寫法,比同時(shí)fork兩個(gè)任務(wù)再join更高效。 - join():等待左任務(wù)完成并獲取結(jié)果,然后合并。
為什么不是先fork兩個(gè)任務(wù)再join?
錯(cuò)誤的寫法:
leftTask.fork(); rightTask.fork(); // 這樣效率低下! Long leftResult = leftTask.join(); Long rightResult = rightTask.join();
這種寫法會(huì)先fork兩個(gè)子任務(wù),然后join等待。問題是:fork之后,兩個(gè)子任務(wù)都進(jìn)入了工作隊(duì)列,等待被其他空閑線程竊取執(zhí)行。如果此時(shí)有空閑線程,沒問題;但如果沒有空閑線程,而當(dāng)前線程又在join等待,就會(huì)浪費(fèi)一個(gè)線程資源。正確的做法是:fork一個(gè)任務(wù),然后當(dāng)前線程同步執(zhí)行另一個(gè)任務(wù),這樣就能確保當(dāng)前線程在等待期間不會(huì)閑著。
3.2 案例二:斐波那契數(shù)列(遞歸任務(wù))
斐波那契數(shù)列是一個(gè)天然的遞歸問題,非常適合用ForkJoin來演示。
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
public class FibonacciTask extends RecursiveTask<Integer> {
private final int n;
public FibonacciTask(int n) {
this.n = n;
}
@Override
protected Integer compute() {
if (n <= 1) {
return n;
}
// 創(chuàng)建子任務(wù):f(n-1) 和 f(n-2)
FibonacciTask f1 = new FibonacciTask(n - 1);
FibonacciTask f2 = new FibonacciTask(n - 2);
// 異步執(zhí)行f1
f1.fork();
// 同步執(zhí)行f2
int result2 = f2.compute();
// 獲取f1的結(jié)果
int result1 = f1.join();
return result1 + result2;
}
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool();
int n = 10; // 計(jì)算第10個(gè)斐波那契數(shù)
int result = pool.invoke(new FibonacciTask(n));
System.out.println("Fibonacci(" + n + ") = " + result); // 輸出55
}
}注意:雖然這個(gè)例子展示了ForkJoin的使用,但斐波那契數(shù)列并不適合用ForkJoin,因?yàn)樗挠?jì)算量太小,而任務(wù)拆分開銷太大,實(shí)際性能比普通遞歸還差。這個(gè)例子僅用于理解API。
3.3 案例三:遍歷目錄統(tǒng)計(jì)文件(RecursiveAction實(shí)戰(zhàn))
這個(gè)案例更有實(shí)際意義:統(tǒng)計(jì)一個(gè)目錄及其子目錄下所有.java文件的數(shù)量。由于不需要返回值,我們使用RecursiveAction。
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
public class FileCounter extends RecursiveAction {
private final File directory;
private final String extension;
private int count = 0; // 統(tǒng)計(jì)結(jié)果
public FileCounter(File directory, String extension) {
this.directory = directory;
this.extension = extension;
}
public int getCount() {
return count;
}
@Override
protected void compute() {
File[] files = directory.listFiles();
if (files == null) return;
List<FileCounter> subTasks = new ArrayList<>();
for (File file : files) {
if (file.isDirectory()) {
// 創(chuàng)建子任務(wù)處理子目錄
FileCounter subTask = new FileCounter(file, extension);
subTask.fork(); // 異步執(zhí)行
subTasks.add(subTask);
} else if (file.getName().endsWith(extension)) {
count++;
}
}
// 等待所有子任務(wù)完成,并累加結(jié)果
for (FileCounter subTask : subTasks) {
subTask.join();
count += subTask.getCount();
}
}
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool();
FileCounter task = new FileCounter(new File("/path/to/your/project"), ".java");
pool.invoke(task); // 同步等待
System.out.println("找到 " + task.getCount() + " 個(gè) .java 文件");
}
}第四部分:適用場景與注意事項(xiàng)
4.1 適用場景
ForkJoin框架最適合解決以下類型的問題:
| 場景類型 | 示例 | 說明 |
|---|---|---|
| 計(jì)算密集型任務(wù) | 大數(shù)組數(shù)學(xué)運(yùn)算、矩陣乘法 | 任務(wù)需要大量CPU計(jì)算,分解后可以并行加速 |
| 可遞歸分解的任務(wù) | 歸并排序、快速排序、文件遍歷 | 天然的分治結(jié)構(gòu) |
| 任務(wù)之間相互獨(dú)立 | 圖像處理(每個(gè)像素獨(dú)立) | 無需同步,沒有數(shù)據(jù)競爭 |
| 任務(wù)粒度適中 | 每個(gè)子任務(wù)計(jì)算量在數(shù)萬到數(shù)百萬次操作 | 太細(xì)則調(diào)度開銷大,太粗則并行度不足 |
4.2 不適用場景
| 場景類型 | 原因 |
|---|---|
| I/O密集型任務(wù) | 線程會(huì)在I/O操作時(shí)阻塞,浪費(fèi)CPU,且工作竊取無法發(fā)揮作用 |
| 需要頻繁同步的任務(wù) | 鎖競爭會(huì)抵消并行帶來的好處 |
| 任務(wù)粒度太細(xì) | 創(chuàng)建任務(wù)、調(diào)度、合并的開銷超過計(jì)算本身 |
| 無法分解的串行任務(wù) | 分治思想的前提就是可以分解 |
對(duì)于I/O密集型任務(wù),可以考慮配合ManagedBlocker使用,或者改用CompletableFuture。
4.3 如何選擇合適的閾值?
閾值的選擇是ForkJoin調(diào)優(yōu)的關(guān)鍵。沒有固定的公式,一般遵循以下原則:
- 通過實(shí)驗(yàn)確定:編寫測(cè)試代碼,對(duì)不同閾值進(jìn)行壓測(cè),找出性能最優(yōu)值。
- 參考經(jīng)驗(yàn)值:對(duì)于簡單的數(shù)組遍歷,閾值在1000-10000之間比較常見。
- 動(dòng)態(tài)調(diào)整:高級(jí)用法中,可以通過
getSurplusQueuedTaskCount()方法判斷當(dāng)前線程的負(fù)載,動(dòng)態(tài)決定是否繼續(xù)拆分。
4.4 常見陷阱與注意事項(xiàng)
陷阱1:在任務(wù)中執(zhí)行阻塞操作
如果在compute()方法中執(zhí)行了Thread.sleep()、等待I/O等阻塞操作,會(huì)導(dǎo)致工作線程被阻塞,無法執(zhí)行其他任務(wù),嚴(yán)重降低并行效率。解決方案是使用ForkJoinPool.ManagedBlocker接口,或者將阻塞部分放在CompletableFuture中處理。
陷阱2:忘記合并結(jié)果
// 錯(cuò)誤的做法:fork了子任務(wù)卻沒有join leftTask.fork(); rightTask.fork(); // 這里應(yīng)該join,但沒有
陷阱3:任務(wù)拆分過深導(dǎo)致棧溢出
遞歸調(diào)用太深可能導(dǎo)致StackOverflowError??梢赃m當(dāng)增大閾值,或者采用非遞歸的實(shí)現(xiàn)方式。
陷阱4:錯(cuò)誤使用invokeAll
invokeAll()方法是批量提交任務(wù)的便捷方式,它會(huì)等待所有任務(wù)完成。但要注意,invokeAll()內(nèi)部已經(jīng)包含了fork操作,不需要再對(duì)子任務(wù)調(diào)用fork。
// 正確用法 invokeAll(leftTask, rightTask); // 然后通過leftTask.join()獲取結(jié)果
陷阱5:忘記處理異常
ForkJoinTask在執(zhí)行過程中可能拋出異常,但異常不會(huì)直接傳播給調(diào)用者。需要通過isCompletedAbnormally()和getException()方法檢查異常。
if (task.isCompletedAbnormally()) {
Throwable ex = task.getException();
ex.printStackTrace();
}
陷阱6:死鎖風(fēng)險(xiǎn)
雖然ForkJoin框架內(nèi)部不會(huì)死鎖,但如果你在任務(wù)中等待其他任務(wù)的結(jié)果時(shí)形成了循環(huán)依賴,仍然可能死鎖。確保任務(wù)依賴關(guān)系是樹形的,而不是環(huán)形的。
4.5 性能優(yōu)化策略
為了充分發(fā)揮ForkJoin的性能,可以采取以下優(yōu)化措施:
- 合理設(shè)置并行度:默認(rèn)等于CPU核心數(shù)。對(duì)于計(jì)算密集型任務(wù),這個(gè)值通常是合適的??梢酝ㄟ^
-Djava.util.concurrent.ForkJoinPool.common.parallelism=N調(diào)整公共池的并行度。 - 使用
compute()而不是fork().join():如前面案例所示,fork一個(gè)任務(wù),同步執(zhí)行另一個(gè),可以減少任務(wù)調(diào)度開銷。 - 避免任務(wù)粒度過細(xì):每個(gè)任務(wù)至少要有數(shù)千次操作,否則調(diào)度開銷會(huì)超過計(jì)算本身。
- 優(yōu)先使用公共池:除非有特殊需求,否則使用
ForkJoinPool.commonPool()。 - 監(jiān)控和調(diào)優(yōu):通過
getPoolSize()、getActiveThreadCount()等方法監(jiān)控線程池狀態(tài),分析性能瓶頸。
第五部分:ForkJoin與現(xiàn)代Java并發(fā)生態(tài)
5.1 Parallel Stream(并行流)
從JDK 8開始,Stream API引入了并行流(.parallelStream()),其底層正是基于ForkJoin框架的公共池實(shí)現(xiàn)的。
// 使用并行流計(jì)算數(shù)組和 long sum = Arrays.stream(array).parallel().sum();
并行流封裝了ForkJoin的復(fù)雜性,讓開發(fā)者可以用聲明式的方式編寫并行代碼。但需要注意的是,并行流默認(rèn)使用公共池,對(duì)于某些I/O操作或阻塞操作,可能不是最佳選擇。
5.2 CompletableFuture
CompletableFuture是JDK 8引入的異步編程工具,它內(nèi)部使用了ForkJoinPool.commonPool()作為默認(rèn)的異步執(zhí)行器。你可以通過thenApplyAsync()、thenComposeAsync()等方法指定使用自定義的線程池,包括ForkJoinPool。
5.3 與其他并發(fā)框架的對(duì)比
| 框架 | 適用場景 | 優(yōu)點(diǎn) | 缺點(diǎn) |
|---|---|---|---|
| ForkJoin | 可分解的計(jì)算密集型任務(wù) | 高效利用CPU,自動(dòng)負(fù)載均衡 | 不適合I/O任務(wù) |
| ThreadPoolExecutor | 通用任務(wù)處理 | 靈活,可定制 | 處理依賴任務(wù)復(fù)雜 |
| CompletableFuture | 異步任務(wù)編排 | 功能強(qiáng)大,支持鏈?zhǔn)秸{(diào)用 | 學(xué)習(xí)曲線較陡 |
| Parallel Stream | 集合數(shù)據(jù)處理 | 聲明式,簡潔 | 控制粒度較粗 |
第六部分:深入源碼(選讀)
6.1 ForkJoinPool的核心數(shù)據(jù)結(jié)構(gòu)
ForkJoinPool內(nèi)部維護(hù)了一個(gè)WorkQueue數(shù)組workQueues。每個(gè)WorkQueue是一個(gè)雙端隊(duì)列,存儲(chǔ)著ForkJoinTask。工作線程與隊(duì)列的對(duì)應(yīng)關(guān)系是:
- 下標(biāo)為奇數(shù)的隊(duì)列:由工作線程獨(dú)占
- 下標(biāo)為偶數(shù)的隊(duì)列:用于存放外部提交的任務(wù)(共享隊(duì)列)
ForkJoinPool還維護(hù)了一個(gè)復(fù)雜的控制信號(hào)量ctl,用于管理線程的狀態(tài)(活躍、等待、終止等)。
6.2 工作竊取的實(shí)現(xiàn)細(xì)節(jié)
當(dāng)工作線程自己的隊(duì)列為空時(shí),會(huì)調(diào)用scan()方法嘗試竊取。它會(huì)隨機(jī)選擇一個(gè)其他線程的隊(duì)列,從尾部獲取一個(gè)任務(wù)。為了防止競爭,這個(gè)操作使用了Unsafe的CAS方法。
如果竊取也失敗,線程會(huì)進(jìn)入等待狀態(tài),將自己掛起。當(dāng)有新任務(wù)提交時(shí),掛起的線程會(huì)被喚醒。
6.3 提交任務(wù)的流程
當(dāng)我們調(diào)用pool.invoke(task)時(shí),流程如下:
- 將任務(wù)放入
ForkJoinPool的外部提交隊(duì)列(偶數(shù)下標(biāo)) - 如果當(dāng)前沒有活躍的工作線程,創(chuàng)建一個(gè)
- 工作線程從隊(duì)列中取出任務(wù)執(zhí)行
- 任務(wù)中的
fork()會(huì)將子任務(wù)放入當(dāng)前工作線程自己的隊(duì)列(奇數(shù)下標(biāo))
到此這篇關(guān)于Java ForkJoin 框架全面解析:分而治之的并行編程藝術(shù)的文章就介紹到這了,更多相關(guān)Java ForkJoin 框架內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Java ForkJoinPool線程池的使用之并行計(jì)算數(shù)組求和實(shí)例
- Java中使用ForkJoinPool的實(shí)現(xiàn)示例
- TransmittableThreadLocal通過javaAgent實(shí)現(xiàn)線程傳遞并支持ForkJoin
- Java線程池ForkJoinPool(工作竊取算法)的使用
- Java?并發(fā)編程之ForkJoin框架
- Java線程池ForkJoinPool實(shí)例解析
- Java ForkJoin框架的原理及用法
- java8中forkjoin和optional框架使用
- Java多線程ForkJoinPool實(shí)例詳解
相關(guān)文章
Java實(shí)現(xiàn)samza轉(zhuǎn)換成flink
將Apache Samza作業(yè)遷移到Apache Flink作業(yè)是一個(gè)復(fù)雜的任務(wù),因?yàn)檫@兩個(gè)流處理框架有不同的API和架構(gòu),本文我們就來看看如何使用Java實(shí)現(xiàn)samza轉(zhuǎn)換成flink吧2024-11-11
Java中InputStream重復(fù)使用問題的幾種解決方案
在Java開發(fā)中,InputStream是用于讀取字節(jié)流的類,在許多場景下,我們可能需要重復(fù)讀取InputStream中的數(shù)據(jù),這篇文章主要介紹了Java中InputStream重復(fù)使用問題的幾種解決方案,需要的朋友可以參考下2025-08-08
Java運(yùn)算符與流程控制之全類型運(yùn)算符用法及分支?/?循環(huán)語句實(shí)戰(zhàn)指南
這篇文章主要介紹了Java運(yùn)算符與流程控制之全類型運(yùn)算符用法及分支/循環(huán)語句實(shí)戰(zhàn)的相關(guān)資料,運(yùn)算符包括算術(shù)、關(guān)系、邏輯、賦值和三元運(yùn)算符,重點(diǎn)講解了它們的用法、優(yōu)先級(jí)和結(jié)合性,以及強(qiáng)制類型轉(zhuǎn)換的注意事項(xiàng),需要的朋友可以參考下2026-01-01
Spring Boot Mysql 數(shù)據(jù)庫操作示例
本篇文章主要介紹了Spring Boot Mysql 數(shù)據(jù)庫操作示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-02-02
Java基本數(shù)據(jù)類型之間的相互轉(zhuǎn)換詳解
這篇文章主要講解Java中基本數(shù)據(jù)類型的轉(zhuǎn)換,數(shù)據(jù)之間相互轉(zhuǎn)換是經(jīng)常會(huì)用到的基礎(chǔ)操作,文中講的很清晰,希望能給大家做一個(gè)參考。2022-05-05
Java基礎(chǔ)學(xué)習(xí)之字符串知識(shí)總結(jié)
今天帶著大家復(fù)習(xí)一下Java基礎(chǔ)知識(shí)-字符串,文中介紹的非常詳細(xì),對(duì)初步學(xué)習(xí)Java或者復(fù)習(xí)Java的小伙伴們都很有幫助喲,需要的朋友可以參考下2021-05-05
idea 隱藏target,iml等不需要展示的文件(推薦)
這篇文章主要介紹了idea 隱藏target,iml等不需要展示的文件,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11
SpringBoot對(duì)靜態(tài)資源的映射規(guī)則詳解解讀
這篇文章主要介紹了SpringBoot對(duì)靜態(tài)資源的映射規(guī)則詳解解讀,在Spring Boot中,映射規(guī)則是用來定義URL與控制器方法之間的映射關(guān)系的,通過映射規(guī)則,可以將特定的URL請(qǐng)求映射到相應(yīng)的控制器方法上,從而實(shí)現(xiàn)請(qǐng)求的處理和響應(yīng)的返回,需要的朋友可以參考下2023-10-10
SpringBoot日志框架之Log4j2快速入門與參數(shù)詳解
本文介紹了SpringBoot日志框架log4j2的基本使用和配置方法,包括將日志輸出到控制臺(tái)、文件、Elasticsearch和Kafka,多個(gè)輸出目的地的配置,異步日志記錄器的使用以及l(fā)og4j2.xml配置文件的詳細(xì)語法和參數(shù)含義,需要的朋友可以參考下2023-05-05

