您的位置:首页 > 其它

ForkJoinTask任务框架简介

2017-12-11 17:34 309 查看
ForkJoinTask是jdk1.7整合Fork/Join,即拆分fork+合并join,性能上有大大提升。

思想:充分利用多核CPU把计算拆分成多个子任务,并行计算,提高CPU利用率大大减少运算时间。有点像,MapReduce思路感觉大致一样。

jdk7中已经提供了最简洁的接口,让你不需要太多时间关心并行时线程的通信,死锁问题,线程同步。
主要技术点

1,ForkJoinTask

     主要使用的方法:

     fork()方法:将任务放入队列并安排异步执行

     join()方法:等待计算完成并返回计算结果。

2,ForkJoinPool

     作为对 Fork/Join 型线程池的实现,是ExecutorService的实现类,因此是一种特殊的线程池。创建ForkJoinPool实例后,可以调用ForkJoinPool的submit(ForkJoinTask<T> task)或者invoke(ForkJoinTask<T> task)来执行指定任务。
注:

ForkJoinPool 相比于 ThreadPoolExecutor,还有一个非常重要的特点(优点)在于,ForkJoinPool具有 Work-Stealing (工作窃取)的能力。所谓 Work-Stealing,在 ForkJoinPool 中的实现为:线程池中每个线程都有一个互不影响的任务队列(双端队列),线程每次都从自己的任务队列的队头中取出一个任务来运行;如果某个线程对应的队列已空并且处于空闲状态,而其他线程的队列中还有任务需要处理但是该线程处于工作状态,那么空闲的线程可以从其他线程的队列的队尾取一个任务来帮忙运行
—— 感觉就像是空闲的线程去偷人家的任务来运行一样,所以叫 “工作窃取”。

Work-Stealing 的适用场景是不同的任务的耗时相差比较大,即某些任务需要运行较长时间,而某些任务会很快的运行完成,这种情况下用 Work-Stealing 很合适;但是如果任务的耗时很平均,则此时 Work-Stealing 并不适合,因为窃取任务时不同线程需要抢占锁,这可能会造成额外的时间消耗,而且每个线程维护双端队列也会造成更大的内存消耗。所以 ForkJoinPool 并不是 ThreadPoolExecutor 的替代品,而是作为对 ThreadPoolExecutor 的补充。

ForkJoinPool 和 ThreadPoolExecutor 都是 ExecutorService(线程池),但ForkJoinPool 的独特点在于:

ThreadPoolExecutor 只能执行 Runnable 和 Callable 任务,而 ForkJoinPool 不仅可以执行 Runnable 和 Callable 任务,还可以执行 Fork/Join 型任务 —— ForkJoinTask —— 从而满足并行地实现分治算法的需要;

ThreadPoolExecutor 中任务的执行顺序是按照其在共享队列中的顺序来执行的,所以后面的任务需要等待前面任务执行完毕后才能执行,而 ForkJoinPool 每个线程有自己的任务队列,并在此基础上实现了 Work-Stealing 的功能,使得在某些情况下 ForkJoinPool 能更大程度的提高并发效率。

RecursiveTask的例子

class PrintTasks extends RecursiveTask<Integer>  {
private static final long serialVersionUID = 1L;

// 每个"小任务"最多只打印4个数
private static final int MAX = 4;

private int start;
private int end;

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

@Override
protected Integer compute() {
Integer cnt =0;
// 当end-start的值小于MAX时候,开始打印
if ((end - start) < MAX) {
for (int i = start; i < end; i++) {
cnt += i;
}
} else {
// 将大任务分解成两个小任务
int middle = (start + end) / 2;
PrintTask left = new PrintTask(start, middle);
PrintTask right = new PrintTask(middle, end);
// 并行执行两个小任务
left.fork();
right.fork();
cnt = left.join()+right.join();
}
return cnt;
}
}

public class CopyOfForkJoinPoolTest {

public static void main(String[] args) throws Exception {
// 创建连接池
ForkJoinPool forkJoinPool = new ForkJoinPool();
// 提交可分解的任务
Future<Integer> result = forkJoinPool.submit(new PrintTasks(0, 10));
// 关闭线程池
forkJoinPool.shutdown();
long endTime = System.currentTimeMillis();
System.out.println("result:"+ result.get());
}

}

RecursiveAction 的例子

class PrintTasks extends RecursiveAction {
private static final long serialVersionUID = 1L;

// 每个"小任务"最多只打印4个数
private static final int MAX = 4;

private int start;
private int end;

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

@Override
protected void compute() {
Integer cnt =0;
// 当end-start的值小于MAX时候,开始打印
if ((end - start) < MAX) {
for (int i = start; i < end; i++) {
cnt += i;
}
} else {
// 将大任务分解成两个小任务
int middle = (start + end) / 2;
PrintTask left = new PrintTask(start, middle);
PrintTask right = new PrintTask(middle, end);
// 并行执行两个小任务
left.fork();
right.fork();
cnt = left.join()+right.join();
}
}
}

public class CopyOfForkJoinPoolTest {

public static void main(String[] args) throws Exception {
// 创建连接池
ForkJoinPool forkJoinPool = new ForkJoinPool();
// 提交可分解的任务
forkJoinPool.submit(new PrintTasks(0, 10));
// 关闭线程池
forkJoinPool.shutdown();
}
}

需要注意的是,在执行子任务时,都使用了下面的写法
// 并行执行两个小任务
left.fork();
right.fork();


这么写,是没有问题的,但是关键是效率低这种写法,意思就是A开了两个子任务B、C,但是A需要等待B、C的结果,在结果出来之前,A的线程就是空闲状态。更好的方式是使用invokeAll,将多个子任务作为参数放进该方法,它会保留一个子任务让当前线程A来执行,其他任务会fork给其他线程来执行。这样就充分利用了线程池,提高工作效率。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  多线程