1. 什么是Fork/Join框架
简单理解就是把一个大任务分割成若干小任务,最终汇总每个小任务结果后得到大任务结果的框架。
打一个非常形象的比喻:比如要计算 从1+2+3+...+10000,那可以分割成10个子任务,每个子任务分别对1000个数进行求和,最终汇总这10个子任务的结果。
2. 工作窃取算法
工作窃取算法是指某个线程从其他队列窃取任务来执行。运行流程图:
从这个图来看非常清楚:
线程1把自己的队列消费完了,线程2还没有处理完,那线程1会把整个队列做完之后再从队列2底部开始往上处理。线程1会帮着线程2一块干活。
优点:充分利用线程并行计算,并减少了线程间的竞争。
缺点:在某些情况下还是存在竞争,比如双端队列里只有一个任务时,并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
3. Fork/Join框架介绍
假如让你来自己编写一个Fork/Join框架,你会怎么设计呢?
我们可以先YY一下,
首先是不是得要先创建N个双端队列(即把一个大任务拆成多个子任务并放入到双端队列里面),然后得创建多个线程每个线程分别处理各自的双端队列
然后当某个线程处理完自己的双端队列之后还要去寻找下一个未完成的双端队列,这个时候这个双端队列可能会有多个线程同时抢占执行的情况,还需要对它加锁。
来看看它是怎么设计的:
第一步:分割任务。就像我上面说的,需要把一个大任务分割成子任务,有可能这个子任务还是很大可能还需要再不停的分割,直到分割出的子任务足够小。
第二步:执行任务并合并结果。分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里面获取任务执行。子任务执行完的结果都统一放在一个队列里。
再启一个Join线程从队列里面拿数据,然后合并这些数据。
Fork/Join使用两个类来完成以上两件事情:
ForkJoinTask:我们需要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务中执行fork()与join()操作的机制、我们只需要继承它的子类,Fork/Join提供了两个子类:
1、RecursiveAction: 用于没有返回结果的任务
2、RecursiveTask: 用于有返回结果的任务
ForkJoinPool: ForkJoinTask需要通过ForkJoinPool来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。
4. 使用Fork/Join框架
比如我们做一个非常简单的示例,计算1+2+3+4的结果。
使用这个框架首先要考虑的是如何进行分割任务。如果我们希望每个子任务最多执行两个数的相加,那么我们设置分割的阀值是2,由于是4个数相加,所以Fork/Join框架会把这个任务fork成两个子任务(总数 / 阀值 = 总子任务数)。子任务负责计算1+2,子任务二负责3+4,然后再Join两个子任务的结果。
因为有结果的任务,所以就必须继承RecursiveTask
代码如下:
public class Chapter4 extends RecursiveTask {
// 阀值
private static final int THRESHOLD = 3;
private long start;
private long end;
public Chapter4(long start, long end) {
this.start = start;
this.end = end;
}
/**
* The main computation performed by this task.
*
* @return the result of the computation
*/
@Override
protected Object compute() {
int sum = 0;
// 如果任务足够小就计算任务
boolean canCompute = (end - start) <= THRESHOLD;
if (canCompute) {
for (long k = start; k <= end; k++) {
sum += k;
}
} else {
// 如果任务大于阀值,就分裂成两个子任务计算
long middle = (start + end) / 2;
Chapter4 leftTask = new Chapter4(start, middle);
Chapter4 rightTask = new Chapter4(middle + 1, end);
// 执行子任务
leftTask.fork();
rightTask.fork();
// 等待子任务执行完,并得到结果
int leftResult = (int)leftTask.join();
int rightResult = (int)rightTask.join();
sum = leftResult + rightResult;
}
return sum;
}
public static void main(String[] args) {
System.out.println(System.currentTimeMillis());
ForkJoinPool forkJoinPool = new ForkJoinPool();
// 生成一个计算任务
Chapter4 task = new Chapter4(1, 2000L);
// 执行一个任务
Future result = forkJoinPool.submit(task);
try {
System.out.println(result.get());
System.out.println(System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
疑问:打断点的时候发现断点不会进入到sum += k; 难道是因为不会进入吗?
ForkJoinTask与一般的任务主要区别在于它需要实现compute方法,在这个方法里,首先需要判断任务是否足够小,如果足够小就直接执行任务,如果不足够小就必须分割成两个子任务,每个子任务在调用fork方法时又会进入compute方法,看看当前子任务是否需要继续分割子任务。如果不需要继续分割则执行当前子任务并返回结果。使用Join方法会等待子任务执行完得到结果。
5. Fork/Join框架
ForkJoinTask在执行的时候可能会抛异常,但我们没办法在主线程里直接捕获异常。所以ForkJoinTask提供了isCompletedAbnormally()方法来检查任务是否已经抛出异常或已取消了,并可以通过ForkJoinTask的getException方法获取异常。代码如下:
protected Object compute() {
int sum = 0;
// 如果任务足够小就计算任务
boolean canCompute = (end - start) <= THRESHOLD;
if (canCompute) {
for (long k = start; k <= end; k++) {
sum += k;
double a = sum / 0; // 抛异常
}
} else {
// 如果任务大于阀值,就分裂成两个子任务计算
long middle = (start + end) / 2;
Chapter4 leftTask = new Chapter4(start, middle);
Chapter4 rightTask = new Chapter4(middle + 1, end);
// 执行子任务
leftTask.fork();
rightTask.fork();
if(leftTask.isCompletedAbnormally()){
System.out.println(leftTask.getException()); // 会捕获到此处的异常
}
if(rightTask.isCompletedAbnormally()){
System.out.println(rightTask.getException());
}
// 等待子任务执行完,并得到结果
int leftResult = (int)leftTask.join();
int rightResult = (int)rightTask.join();
sum = leftResult + rightResult;
}
return sum;
}
ForkJoinPool由ForkJoinTask数组和ForkJoinWorkerThread数组组成,ForkJoinTask数组负责存放程序提交给ForkJoinPool的任务,而ForkJoinWorkerThread数组负责执行这些任务。
ForkJoinTask的fork方法实现原理,当我们调用ForkJoinTask的fork方法时,程序会调用ForkJoinWorkerThread的pushTask方法异步的执行这个任务,然后立即返回结果
pushTask方法把当前任务存放在ForkJoinTask数组queue里,然后再调用ForkJoinPool的signalWork方法唤醒或创建一个工作线程来执行任务
Join方法的主要作用是阻塞当前线程并等待获取结果。
示例2. 入参是一个列表,需要对这个列表进行分片处理,再对结果进行合并处理
public class ListForkJoin extends RecursiveAction {
private static final int THRESHOLD = 4;
private List<Integer> targetList;
private int start;
private int end;
private static List<Integer> resultList = new ArrayList<>();
public ListForkJoin(List<Integer> array, int start, int end) {
this.targetList = array;
this.start = start;
this.end = end;
}
/***
* 单个线程处理这个列表里面的数据
* @param item
*/
public void dealList(List<Integer> item) {
for (Integer i : item
) {
resultList.add(i * 2);
}
}
/**
* The main computation performed by this task.
*/
@Override
protected void compute() {
if ((end - start) <= THRESHOLD) {
// 小于阀值则直接调用这个方法进行计算
//dealList(targetList[start,end]);
System.out.println("!!!!" + start + end);
dealList(targetList.subList(start, end));
} else {
System.out.println(start + "_" + end + "切分");
// 一个大任务分割成两个子任务
int mid = (start + end) / 2;
ListForkJoin left = new ListForkJoin(targetList, start, mid);
ListForkJoin right = new ListForkJoin(targetList, mid, end);
// 分别并行计算
invokeAll(left, right);
left.join();
right.join();
}
}
private static List<Integer> genArray() {
List<Integer> result = new ArrayList<>();
for (int i = 0; i < 200; i++) {
result.add(new Random().nextInt(500));
}
return result;
}
public static void main(String[] args) throws InterruptedException {
while (true) {
resultList = new ArrayList<>();
List<Integer> va = genArray();
ForkJoinPool forkJoinPool = new ForkJoinPool();
// 生成一个计算任务
ListForkJoin task = new ListForkJoin(va, 0, va.size());
// 执行一个任务
Future result = forkJoinPool.submit(task);
try {
result.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println(resultList);
System.out.println(resultList.size());
Thread.sleep(100000);
}
}
}
有了上面的这个示例说明之后就可以用这种方式来处理从ODPS读到大批数据之后并发处理这些数据了.
示例代码如下:
参考链接:
http://www.javacreed.com/java-fork-join-example/