fork/join 分而治之
1.什么是Fork/join框架?
从JDK1.7开始,Java提供Fork/Join框架用于并行执行任务。它的思想就是讲一个大任务分割成若干小任务,最终汇总每个小任务的结果得到这个大任务的结果。如下图:
2.什么是分而治之思想
可以简单的理解为:将规模为N的问题,当N<阈值,直接解决;当N>阈值,将N分解为K个小规模子问题,子问题互相对立,与原问题形式相同,将子问题的解合并得到原问题的解。
3. 工作窃取(work-stealing)
假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。如果现场A的任务执行完成后,于是它就去其他线程B的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行,也就是线程B从头部拿取,而线程A从尾部获取任务开始执行。
工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
4.如何实现Fork/join
java 的ForkJoinTask 抽象类中提供compute方法给我们实现这种思想。java又提供两个抽象类继承ForkJoinTask ,分别是RecursiveAction和RecursiveTask.他们的区别在于:RecursiveTask有返回值,RecursiveAction无返回值。
4.1 fork/join标准规范
4.2 RecursiveTask的有返回值实现:
接下来我们以这样的例子,Fork/Join的同步用法实现有返回结果值:统计整形数组中所有元素的和
//RecursiveTask有返回值
public class FaceCompareTask extends RecursiveTask<FaceFeature> {
private static final Logger logger = LoggerFactory.getLogger(FaceCompareTask.class);
private final Spliterator<FaceFeature> spliterator;
//是否找到标识
private final AtomicBoolean flag;
//识别目标
private final FaceFeature target;
private final FaceEngine engine;
private final float score;
public FaceCompareTask(Spliterator<FaceFeature> spliterator, AtomicBoolean flag, FaceFeature target, FaceEngine engine, float score) {
this.spliterator = spliterator;
this.flag = flag;
this.target = target;
this.engine = engine;
this.score = score;
}
@Override
protected FaceFeature compute() {
long listSize = spliterator.estimateSize();
//数据分段
if (listSize > 100) {
FaceCompareTask left = new FaceCompareTask(this.spliterator, flag, target, engine, score);
FaceCompareTask right = new FaceCompareTask(this.spliterator.trySplit(), flag, target, engine, score);
invokeAll(left,right);
FaceFeature leftResult = left.join();
FaceFeature rightResult = right.join();
return leftResult != null ? leftResult : rightResult;
} else {
List<FaceFeature> list = new ArrayList<>();
spliterator.forEachRemaining(list::add);
FaceFeature result = null;
for (FaceFeature faceFeature : list) {
//其他线程已找到
if (flag.get()) {
break;
}
FaceSimilar similar = new FaceSimilar();
int code = engine.compareFaceFeature(faceFeature, target, similar);
/*if (code != 0 && logger.isDebugEnabled()) {
logger.debug("compare match error:" + code);
}*/
//找到后,将标识设为true
if (similar.getScore() >= score) {
flag.set(true);
result = faceFeature;
break;
}
}
return result;
}
}
}
4.2 RecursiveAction的无返回值实现:
例子是Fork/Join的异步用法不要求返回值,控制台打印结果:遍历指定目录(含子目录)寻找指定txt结尾的文件
public class RecursiveTaskTest {
public static void main(String[] args)throws Exception{
// 创建包含Runtime.getRuntime().availableProcessors()返回值作为个数的并行线程的ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool();
List<Integer> handlerList = new ArrayList<>();
for(int i=1;i<=1000;i++){
handlerList.add(i);
}
// 提交可分解的PrintTask任务
ForkJoinTask<Void> submit = forkJoinPool.submit(new RaskDemo(handlerList.spliterator()));
//阻塞当前线程直到 ForkJoinPool 中所有的任务都执行结束
submit.get();//获取结果:一直等待(相当于主线程阻塞),直到能获取到子线程执行的结果。
// 关闭线程池
//forkJoinPool.shutdown();
}
}
class RaskDemo extends RecursiveAction {
/**
* 每个"小任务"最多只打印20个数
*/
private static final int MAX = 20;
private Spliterator<Integer> spliterator;
public RaskDemo(Spliterator<Integer> spliterator) {
this.spliterator = spliterator;
}
@Override
protected void compute() {
long leastSize = spliterator.estimateSize();
//当end-start的值小于MAX时,开始打印
if (leastSize < MAX) {
List<Integer> list = new ArrayList<>();
spliterator.forEachRemaining(list::add);
list.forEach(integer -> {
System.out.println(Thread.currentThread().getName() + "i的值" + integer);
});
} else {
RaskDemo left = new RaskDemo(spliterator);
RaskDemo right = new RaskDemo(spliterator.trySplit());
left.fork();
right.fork();
}
}
}
}
1,invokeAll(task)方法,主动执行其它的ForkJoinTask,并等待Task完成。(同步的)
2,fork方法,让一个task执行(异步的)
3,join方法,让一个task执行(同步的,它和fork不同点是同步或者异步的区别)
4,可以使用join来取得ForkJoinTask的返回值。由于RecursiveTask类实现了Future接口,所以也可以使用get()取得返回值。
get()和join()有两个主要的区别:
join()方法不能被中断。如果你中断调用join()方法的线程,这个方法将抛出InterruptedException异常。
如果任务抛出任何未受检异常,get()方法将返回一个ExecutionException异常,而join()方法将返回一个RuntimeException异常。
5,ForkJoinTask在不显示使用ForkJoinPool.execute/invoke/submit()方法进行执行的情况下,也可以使用自己的fork/invoke方法进行执行。
使用fork/invoke方法执行时,其实原理也是在ForkJoinPool里执行,只不过使用的是一个“在ForkJoinPool内部生成的静态的”ForkJoinPool。
6,ForkJoinTask有两个子类,RecursiveAction和RecursiveTask。他们之间的区别是,RecursiveAction没有返回值,RecursiveTask有返回值。
7,看看ForkjoinTask的Complete方法的使用场景
这个方法好要是用来使一个任务结束。这个方法被用在结束异步任务上,或者为那些能不正常结束的任务,提供一个选择。
8,Task的completeExceptionally方法是怎么回事。
这个方法被用来,在异步的Task中产生一个exception,或者强制结束那些“不会结束”的任务
这个方法是在Task想要“自己结束自己”时,可以被使用。而cancel方法,被设计成被其它TASK调用。
当你在一个任务中抛出一个未检查异常时,它也影响到它的父任务(把它提交到ForkJoinPool类的任务)和父任务的父任务,以此类推。
9,可以使用ForkJoinPool
.execute(异步,不返回结果)、
invoke(同步,返回结果)、
submit(异步,返回结果)方法,来执行ForkJoinTask。
10,ForkJoinPool有一个方法commonPool(),这个方法返回一个ForkJoinPool内部声明的静态ForkJoinPool实例。 在jdk1.8里面才有。文档上说,这个方法适用于大多数的应用。这个静态实例的初始线程数,为“CPU核数-1 ”(Runtime.getRuntime().availableProcessors() - 1)。
参考
2、https://www.jianshu.com/p/32a15ef2f1bf
3、https://www.jianshu.com/p/de025df55363