ForkJoin使用,模拟数据库查询操作
手上的工作需要实现从数据库中查询大量的数据,然后将数据整合进行分页,于是了解使用ForkJoin框架,将数据库按条件分页建立索引,以分页为每次查询最大限度来进行查询,最后将查询结果整合,实现对数据查询速度的优化。
ForkJoin框架
从JDK1.7开始,Java提供Fork/Join框架用于并行执行任务,它的思想就是讲一个大任务分割成若干小任务,最终汇总每个小任务的结果得到这个大任务的结果。
这种思想和MapReduce很像(input --> split --> map --> reduce --> output)
主要有两步:
第一、任务切分;
第二、结果合并
它的模型大致是这样的:线程池中的每个线程都有自己的工作队列(PS:这一点和ThreadPoolExecutor不同,ThreadPoolExecutor是所有线程公用一个工作队列,所有线程都从这个工作队列中取任务),当自己队列中的任务都完成以后,会从其它线程的工作队列中偷一个任务执行,这样可以充分利用资源。
工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。工作窃取的运行流程图如下:
假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
API介绍:
ForkJoinPool与其它的ExecutorService区别主要在于它使用“工作窃取”:线程池中的所有线程都企图找到并执行提交给线程池的任务。当大量的任务产生子任务的时候,或者同时当有许多小任务被提交到线程池中的时候,这种处理是非常高效的。特别的,当在构造方法中设置asyncMode为true的时候这种处理更加高效。
ForkJoinTask代表运行在ForkJoinPool中的任务。
ForkJoinTask中的主要方法:
fork() 在当前线程运行的线程池中安排一个异步执行。简单的理解就是再创建一个子任务。
join() 当任务完成的时候返回计算结果。
invoke() 开始执行任务,如果必要,等待计算完成。
子类:
RecursiveAction 一个递归无结果的ForkJoinTask(没有返回值)
RecursiveTask 一个递归有结果的ForkJoinTask(有返回值)
ForkJoinWorkerThread代表ForkJoinPool线程池中的一个执行任务的线程。
类结构图:
使用ForkJoin处理过程
代码示例
1.实现1~100相加
package forkJoin;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;
/**
* @Author yangwen-bo
* @Date 2020/6/8.
* @Version 1.0
*
* 初步简单使用forkjoin
* 使用forkjoin实现1~100求和
*/
public class ForkJoinDemo {
/**
* ForkJoinPool是最外层运行任务的“池子”
*
* ForkJoinPool由ForkJoinTask数组和ForkJoinWorkerThread数组组成,
* ForkJoinTask数组负责存放程序提交给ForkJoinPool的任务,
* 而ForkJoinWorkerThread数组负责执行这些任务
*
* ForkJoinTask就是ForkJoinPool里面的每一个任务。
* 他主要有两个子类:
* RecursiveAction(一个递归无结果的ForkJoinTask(没有返回值))
* 和RecursiveTask(一个递归有结果的ForkJoinTask(有返回值))。
* 然后通过fork()方法去分配任务执行任务,通过join()方法汇总任务结果
*
* ForkJoinTask在执行的时候可能会抛出异常,在主线程中是无法直接获取的,
* 但是可以通过ForkJoinTask提供的isCompletedAbnormally()方法来检查任务是否已经抛出异常或已经被取消了
*
*/
/**
* RecursiveAction是没有返回值的 compute处理后没有返回值
* ForkJoinPool submit之后没有返回值,
* 可以通过forkJoinPool.awaitQuiescence( 100, TimeUnit.MILLISECONDS )阻塞当前线程直到 ForkJoinPool 中所有的任务都执行结束
*
*
*/
// RecursiveTask一个递归有结果的ForkJoinTask(有返回值)
private static class MyRecursiveTask extends RecursiveTask<Integer>{
//规定的任务拆分最小值
private final int THRESHOLD = 10;
private final int start;
private final int end;
public MyRecursiveTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
//使用二分法细分任务,如果任务小的不能再细分,则直接计算
if (end-start<=THRESHOLD){
// 任务已不能拆分,直接计算
// 这里是求给定两个数之间的数求和,rangeClosed包含结束节点
return IntStream.rangeClosed( start,end )