1 工作原理
1.1 核心思想
Fork/Join
框架是Java 7
提供的一个用于并行执行任务的框架, 核心思想就是把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果,其实现思想与MapReduce有异曲同工之妙。
比如,计算1+2+…+10000,可以分割成10个子任务,每个子任务分别对1000个数进行求和,最终汇总这10个子任务的结果。
Fork/Join
的运行流程图如下:
![](/i/l/?n=23&i=blog/1173617/202302/1173617-20230211142344381-47790260.png)
1.2 工作窃取算法
Fork/Join
框架使用一个巧妙的算法来平衡线程的负载,称为工作窃取(work-stealing)算法
。工作窃取的运行流程图如下:
![](/i/l/?n=23&i=blog/1173617/202302/1173617-20230211142503190-1864817644.png)
假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。
但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。
而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列。被窃取任务线程永远从双端队列的头部
拿任务执行,而窃取任务的线程永远从双端队列的尾部
拿任务执行。
工作窃取算法
的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如:双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
1.3 内部构成
Fork/Join框架主要由两部分组成(分别使用2个类来实现):
1.3.1 分割任务(ForkJoinTask)
分割任务
。首先,我们需要有一个fork类来把大任务分割成子任务,有可能子任务还是很大,所以还需要不停的分割,直到分割出的子任务足够小。
我们要使用
ForkJoin框架
,必须首先创建一个ForkJoin
任务。它提供在任务中执行fork()
和join()
操作的机制。通常情况下,我们不需要直接继承ForkJoinTask
类,而只需要继承其子类,Fork/Join框架提供了以下两个子类:
- RecursiveAction:用于没有返回结果的任务。
- RecursiveTask :用于有返回结果的任务。
1.3.2 执行任务并合并结果(ForkJoinPool)
执行任务并合并结果
。分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。
ForkJoinPool
需要通过ForkJoinPool
来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。
1.4 源码解读【待完善】
![](/i/l/?n=23&i=blog/1173617/202302/1173617-20230211143849283-1857256890.png)
1.4.1 ForkJoinPool
ForkJoinPool
代表一个需要执行的任务,真正执行这些任务的线程放在一个里面,是一个可以执行的ExcuteService
。但不同的是,它采用了work-stealing
模式:所有在池中的空闲线程尝试去执行其他线程创建的子任务,这样就很少有线程处于空闲状态,非常高效。
池中维护着ForkJoinWorkerThread
对象数组:
/**
* Array holding all worker threads in the pool. Initialized upon
* construction. Array size must be a power of two. Updates and
* replacements are protected by scanGuard, but the array is
* always kept in a consistent enough state to be randomly
* accessed without locking by workers performing work-stealing,
* as well as other traversal-based methods in this class, so long
* as reads memory-acquire by first reading ctl. All readers must
* tolerate that some array slots may be null.
*/
ForkJoinWorkerThread[] workers;
ForkJoinWorkerThread
为任务的执行线程,workers
数组在构造方法中初始化,其大小必须为2的n次方(方便将取模转换为移位运算)。
ForkJoinPool初始化方法:
2 案例演示
案例1
让我们通过一个简单的需求来使用下Fork/Join框架
- 需求:计算1~8的累加结果。
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;
/**
* @author johnny-zen
* @create-time 2023/2/11 13:54
* @description ...
* @reference-doc
* [1] 好文推荐:JAVA基础之Fork/Join框架 - baidu - https://baijiahao.baidu.com/s?id=1709429670171703195
*/
public class CountTask extends RecursiveTask<Integer> {
private static final int THRESHHOLD = 2;
private int start;
private int end;
public CountTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
System.out.println(start + " - " + end + " begin");
int sum = 0;
boolean canCompute = (end - start) <= THRESHHOLD;
if (canCompute) { // 达到了计算条件,则直接执行
for (int i = start; i <= end; i++) {
sum += i;
}
} else { // 不满足计算条件,则分割任务
int middle = (start + end) / 2;
CountTask leftTask = new CountTask(start, middle);
CountTask rightTask = new CountTask(middle + 1, end);
leftTask.fork(); // 执行子任务
rightTask.fork();
int leftResult = leftTask.join(); // 等待子任务执行完毕
int rightResult = rightTask.join();
sum = leftResult + rightResult; // 合并子任务的计算结果
}
System.out.println(start + + end + " end");
return sum;
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
ForkJoinPool pool = new ForkJoinPool();
CountTask task = new CountTask(1, 8);
Future<Integer> future = pool.submit(task);
//在执行的时候可能会抛出异常,但我们没办法在主线程里直接捕获异常,所以提供了 isCompletedAbnormally() 方法来检查任务是否已经抛出异常或已经被取消了,且可通过 getException方法 获取异常
if (task.isCompletedAbnormally()) {//检查任务是否已经抛出异常或已经被取消了
//方法返回Throwable对象,如果任务被取消了则返回CancellationException。如果任务没有完成或者没有抛出异常则返回 null
Throwable exception = task.getException();
System.out.println(exception);
} else {
// 由于每个任务是由线程池执行的,每次的执行顺序会有不同,但是,父任务肯定在所有子任务之后完成
// 比如1-8的计算肯定在子任务1-4、5-8之后完成,但是1-4、5-8的完成顺序是不确定的。
System.out.println("result: " + future.get());//36
}
}
}
output
1 - 8 begin
5 - 8 begin
1 - 4 begin
1 - 2 begin
5 - 6 begin
3 end
11 end
3 - 4 begin
7 - 8 begin
15 end
13 end
7 end
5 end
9 end
result: 36
X 参考文献
- 好文推荐:JAVA基础之Fork/Join框架 - Baidu 【待继续阅读】