Java 多线程中的任务分解机制-ForkJoinPool,以及CompletableFuture
简介
ForkJoinPool的优势在于,可以充分利用多cpu
,多核cpu
的优势,把一个任务拆分成多个“小任务”,把多个“小任务”放到多个处理器核心上并行执行;当多个“小任务”执行完成之后,再将这些执行结果合并起来即可。
Java7
提供了ForkJoinPool
来支持将一个任务拆分成多个“小任务”并行计算,再把多个“小任务”的结果合并成总的计算结果。
ForkJoinPool
是ExecutorService
的实现类,因此是一种特殊的线程池。
使用方法:创建了ForkJoinPool
实例之后,就可以调用ForkJoinPool
的submit(ForkJoinTask<T> task)
或invoke(ForkJoinTask<T> task)
方法来执行指定任务了。
其中ForkJoinTask
代表一个可以并行、合并的任务。ForkJoinTask
是一个抽象类,它还有两个抽象子类:RecusiveAction和RecusiveTask。其中RecusiveTask代表有返回值的任务,而RecusiveAction代表没有返回值的任务。
ForkJoinTask:
RecusiveAction实现方法:
package com.qhong.thread.ForkJoinPoolDemo;
import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.TimeUnit;
public class ForkJoinPoolDemo extends RecursiveAction {
private static final long serialVersionUID = 1L;
//定义一个分解任务的阈值——50,即一个任务最多承担50个工作量
private int THRESHOLD=50;
//任务量
private int task_Num=0;
ForkJoinPoolDemo(int Num){
this.task_Num=Num;
}
public static void main (String[] args) throws Exception {
//创建一个支持分解任务的线程池ForkJoinPool
ForkJoinPool pool=new ForkJoinPool();
ForkJoinPoolDemo task=new ForkJoinPoolDemo(120);
pool.submit(task);
pool.awaitTermination(20, TimeUnit.SECONDS);//等待20s,观察结果
pool.shutdown();
}
/**
* @author qhong
* @param
* @return
* @date 2018/4/18 17:13
* @description 实现recursiveAction中抽象方法
*/
@Override
protected void compute() {
if(task_Num<=THRESHOLD){
System.out.println(Thread.currentThread().getName()+"承担了"+task_Num+"份工作");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
//随机解成两个任务
Random m=new Random();
int x=m.nextInt(50);
ForkJoinPoolDemo left=new ForkJoinPoolDemo(x);
ForkJoinPoolDemo right=new ForkJoinPoolDemo(task_Num-x);
left.fork();
right.fork();
}
}
}
Output:
ForkJoinPool-1-worker-1承担了6份工作
ForkJoinPool-1-worker-2承担了2份工作
ForkJoinPool-1-worker-3承担了30份工作
ForkJoinPool-1-worker-0承担了9份工作
ForkJoinPool-1-worker-1承担了46份工作
ForkJoinPool-1-worker-2承担了17份工作
ForkJoinPool-1-worker-0承担了0份工作
ForkJoinPool-1-worker-3承担了10份工作
RecusiveTask的具体实现:
package com.qhong.thread.ForkJoinPoolDemo;
import java.util.Arrays;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
import java.util.stream.LongStream;
/**
* @author qhong
* @date 2018/4/18 16:14
* @description
**/
public class ForkJoinCalculator implements Calculator {
private ForkJoinPool pool;
public ForkJoinCalculator() {
// 也可以使用公用的 ForkJoinPool:
// pool = ForkJoinPool.commonPool()
pool = new ForkJoinPool();
}
public static void main(String[] args) {
ForkJoinCalculator forkJoinCalculator=new ForkJoinCalculator();
long[] numbers=LongStream.range(1,20).toArray();
System.out.println(Arrays.toString(numbers));
long result=forkJoinCalculator.sumUp(numbers);
System.out.println("result:"+result);
}
private static class SumTask extends RecursiveTask<Long> {
private long[] numbers;
private int from;
private int to;
public SumTask(long[] numbers, int from, int to) {
this.numbers = numbers;
this.from = from;
this.to = to;
}
@Override
protected Long compute() {
// 当需要计算的数字小于6时,直接计算结果
if (to - from < 4) {
long total = 0;
for (int i = from; i <= to; i++) {
total += numbers[i];
}
System.out.println(String.format("currentThread:%s,total:%s,from:%s,to:%s",Thread.currentThread().getName(),total,from,to));
return total;
// 否则,把任务一分为二,递归计算
} else {
int middle = (from + to) / 2;
SumTask taskLeft = new SumTask(numbers, from, middle);
SumTask taskRight = new SumTask(numbers, middle+1, to);
taskLeft.fork();
taskRight.fork();
return taskLeft.join() + taskRight.join();
}
}
}
@Override
public long sumUp(long[] numbers) {
return pool.invoke(new SumTask(numbers, 0, numbers.length-1));
}
}
Output:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
currentThread:ForkJoinPool-1-worker-2,total:6,from:0,to:2
currentThread:ForkJoinPool-1-worker-3,total:36,from:10,to:12
currentThread:ForkJoinPool-1-worker-2,total:9,from:3,to:4
currentThread:ForkJoinPool-1-worker-3,total:29,from:13,to:14
currentThread:ForkJoinPool-1-worker-2,total:21,from:5,to:7
currentThread:ForkJoinPool-1-worker-3,total:70,from:15,to:18
currentThread:ForkJoinPool-1-worker-2,total:19,from:8,to:9
result:190
分析:
根据上面的示例代码,可以看出 fork()
和 join()
是 Fork/Join Framework
“魔法”的关键。我们可以根据函数名假设一下 fork()
和 join()
的作用:
fork()
:开启一个新线程(或是重用线程池内的空闲线程),将任务交给该线程处理。join()
:等待该任务的处理线程处理完毕,获得返回值。
并不是每个 fork()
都会促成一个新线程被创建,而每个 join()
也不是一定会造成线程被阻塞。
Fork/Join Framework
的实现算法并不是那么“显然”,而是一个更加复杂的算法——这个算法的名字就叫做 work stealing 算法。
ForkJoinPool
的每个工作线程都维护着一个工作队列(WorkQueue
),这是一个双端队列(Deque
),里面存放的对象是任务(ForkJoinTask
)。- 每个工作线程在运行中产生新的任务(通常是因为调用了
fork()
)时,会放入工作队列的队尾,并且工作线程在处理自己的工作队列时,使用的是LIFO
方式,也就是说每次从队尾取出任务来执行。 - 每个工作线程在处理自己的工作队列同时,会尝试窃取一个任务(或是来自于刚刚提交到
pool
的任务,或是来自于其他工作线程的工作队列),窃取的任务位于其他线程的工作队列的队首,也就是说工作线程在窃取其他工作线程的任务时,使用的是FIFO
方式。 - 在遇到
join()
时,如果需要join
的任务尚未完成,则会先处理其他任务,并等待其完成。 - 在既没有自己的任务,也没有可以窃取的任务时,进入休眠。
fork()
做的工作只有一件事,既是把任务推入当前工作线程的工作队列里。
join()
的工作则复杂得多,也是 join()
可以使得线程免于被阻塞的原因——不像同名的 Thread.join()
。
- 检查调用
join()
的线程是否是ForkJoinThread
线程。如果不是(例如main
线程),则阻塞当前线程,等待任务完成。如果是,则不阻塞。 - 查看任务的完成状态,如果已经完成,直接返回结果。
- 如果任务尚未完成,但处于自己的工作队列内,则完成它。
- 如果任务已经被其他的工作线程偷走,则窃取这个小偷的工作队列内的任务(以
FIFO
方式),执行,以期帮助它早日完成欲join
的任务。 - 如果偷走任务的小偷也已经把自己的任务全部做完,正在等待需要 join 的任务时,则找到小偷的小偷,帮助它完成它的任务。
- 递归地执行第
5
步。
所谓work-stealing模式,即每个工作线程都会有自己的任务队列。当工作线程完成了自己所有的工作后,就会去“偷”别的工作线程的任务。
假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
submit:
其实除了前面介绍过的每个工作线程自己拥有的工作队列以外,ForkJoinPool
自身也拥有工作队列,这些工作队列的作用是用来接收由外部线程(非 ForkJoinThread
线程)提交过来的任务,而这些工作队列被称为 submitting queue
。
submit()
和 fork()
其实没有本质区别,只是提交对象变成了 submitting queue
而已(还有一些同步,初始化的操作)。submitting queue
和其他 work queue
一样,是工作线程”窃取“的对象,因此当其中的任务被一个工作线程成功窃取时,就意味着提交的任务真正开始进入执行阶段。
ForkJoinPool与ThreadPoolExecutor区别:
1.ForkJoinPool
中的每个线程都会有一个队列,而ThreadPoolExecutor
只有一个队列,并根据queue
类型不同,细分出各种线程池
2.ForkJoinPool
能够使用数量有限的线程来完成非常多的具有父子关系的任务,ThreadPoolExecutor
中根本没有什么父子关系任务
3.ForkJoinPool
在使用过程中,会创建大量的子任务,会进行大量的gc,但是ThreadPoolExecutor
不需要,因此单线程(或者任务分配平均)
4.ForkJoinPool
在多任务,且任务分配不均是有优势,但是在单线程或者任务分配均匀的情况下,效率没有ThreadPoolExecutor
高,毕竟要进行大量gc
子任务
ForkJoinPool
在多线程情况下,能够实现工作窃取(Work Stealing
),在该线程池的每个线程中会维护一个队列来存放需要被执行的任务。当线程自身队列中的任务都执行完毕后,它会从别的线程中拿到未被执行的任务并帮助它执行。
ThreadPoolExecutor
因为它其中的线程并不会关注每个任务之间任务量的差异。当执行任务量最小的任务的线程执行完毕后,它就会处于空闲的状态(Idle
),等待任务量最大的任务执行完毕。
因此多任务在多线程中分配不均时,ForkJoinPool
效率高。
stream中应用ForkJoinPool
Arrays.asList("a1", "a2", "b1", "c2", "c1")
.parallelStream()
.filter(s -> {
System.out.format("filter: %s [%s]\n",
s, Thread.currentThread().getName());
return true;
})
.map(s -> {
System.out.format("map: %s [%s]\n",
s, Thread.currentThread().getName());
return s.toUpperCase();
})
.sorted((s1, s2) -> {
System.out.format("sort: %s <> %s [%s]\n",
s1, s2, Thread.currentThread().getName());
return s1.compareTo(s2);
})
.forEach(s -> System.out.format("forEach: %s [%s]\n",
s, Thread.currentThread().getName()));
parallelStream
让部分Java
代码自动地以并行的方式执行
最后:
有一点要注意,就是手动设置ForkJoinPool
的线程数量时,实际线程数为设置的线程数+1
,因为还有一个main
主线程
即使将ForkJoinPool
的通用线程池的线程数量设置为1,实际上也会有2个工作线程。因此线程数为1的ForkJoinPool
通用线程池和线程数为2的ThreadPoolExecutor
是等价的。
与ForkJoinPool对应的是CompletableFuture
Future
以及相关使用方法提供了异步执行任务的能力,但是对于结果的获取却是很不方便,只能通过阻塞或者轮询的方式得到任务的结果。
阻塞的方式显然和我们的异步编程的初衷相违背,轮询的方式又会耗费无谓的CPU
资源,而且也不能及时地得到计算结果
CompletableFuture
就是利用观察者设计模式当计算结果完成及时通知监听者
在Java 8
中, 新增加了一个包含50
个方法左右的类: CompletableFuture
,提供了非常强大的Future
的扩展功能,可以帮助我们简化异步编程的复杂性,提供了函数式编程的能力,可以通过回调的方式处理计算结果,并且提供了转换和组合CompletableFuture
的方法。
具体讲解链接:http://colobu.com/2016/02/29/Java-CompletableFuture/
http://colobu.com/2018/03/12/20-Examples-of-Using-Java%E2%80%99s-CompletableFuture/
参考:
http://www.cnblogs.com/lixuwu/p/7979480.html#undefined
http://blog.dyngr.com/blog/2016/09/15/java-forkjoinpool-internals/