ForkJoin

ForkJoinPool 是 JDK 7 中,@author Doug Lea 加入的一个线程池类。Fork/Join 框架的核心原理就是分治算法(Divide-and-Conquer)和工作窃取算法(work-stealing algorithm)。

Fork分解任务成独立的子任务,用多线程去执行这些子任务,Join合并子任务的结果。这样就能使用多线程的方式来执行一个任务。

JDK7引入的Fork/Join有三个核心类:

  • ForkJoinPool,执行任务的线程池。
  • ForkJoinWorkerThread,执行任务的工作线程。
  • ForkJoinTask,一个用于ForkJoinPool的任务抽象类。它提供了很多方法,但核心的是fork()和join()方法,承载着主要的任务协调工作,fork()用于任务提交,join()用于结果获取。

因为ForkJoinTask比较复杂,抽象方法比较多,日常使用时一般不会继承ForkJoinTask来实现自定义的任务,而是继承ForkJoinTask的两个子类:

  • RecursiveTask:子任务带返回结果时使用。
  • RecursiveAction:子任务不带返回结果时使用。

ForkJoinPool 最适合的是计算密集型的任务,如果存在 I/O,线程间同步,sleep() 等会造成线程长时间阻塞的情况时,最好配合使用 ManagedBlocker。

ForkJoinPool 分治算法思想

分治(divide and conquer),也就是把一个复杂的问题分解成相似的子问题,然后子问题再分子问题,直到问题分的无法再划分了。然后层层返回子问题的结果,最终合并返回问题结果。

分治在算法上有很多应用,类似大数据的MapReduce,归并算法、快速排序算法等。JUC中的Fork/Join的并行计算框架类似于单机版的 MapReduce。

我们常用的数组工具类 Arrays 在JDK 8之后新增的并行排序方法(parallelSort)就运用了 ForkJoinPool 的特性,还有 ConcurrentHashMap 在JDK 8之后添加的函数式方法(如forEach等)也有运用。在整个JUC框架中,ForkJoinPool 相对其他类会复杂很多。

工作窃取算法(work-stealing)

ForkJoinPool 的核心特性是它使用了work-stealing(工作窃取)算法:线程池内的所有工作线程都尝试找到并执行已经提交的任务,或者是被其他活动任务创建的子任务(如果不存在就阻塞等待)。

这种特性使得 ForkJoinPool 在运行多个可以产生子任务的任务,或者是提交的许多小任务时效率更高。尤其是构建异步模型的 ForkJoinPool 时,对不需要合并(join)的事件类型任务也非常适用。

  • 工作窃取算法的优点是充分利用线程进行并行计算,从尾部窃取任务减少了线程间的竞争。
  • 工作窃取算法缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。

在 ForkJoinPool 中,线程池中每个工作线程(ForkJoinWorkerThread)都对应一个任务队列(WorkQueue),工作线程优先处理来自自身队列的任务(LIFO或FIFO顺序,参数 mode 决定),然后以FIFO的顺序随机窃取其他队列中的任务。

ForkJoinPool 中的任务分为两种:一种是本地提交的任务(Submission task,如 execute、submit 提交的任务);另外一种是 fork 出的子任务(Worker task)。两种任务都会存放在 WorkQueue 数组中,但是这两种任务并不会混合在同一个队列里,ForkJoinPool 内部使用了一种随机哈希算法(有点类似 ConcurrentHashMap 的桶随机算法)将工作队列与对应的工作线程关联起来,Submission 任务存放在 WorkQueue 数组的偶数索引位置,Worker 任务存放在奇数索引位。

示例

public class LongSum extends RecursiveTask<Long> {
    // 任务拆分最小阈值
    static final int SEQUENTIAL_THRESHOLD = 10000000;

    // 记录每个任务中元素的起始和终止位置
    // 如果任务中的元素个数超过了拆分的最小阈值就会进一步拆分
    // 直到被拆成最小的任务
    int low;
    int high;
    int[] array;

    LongSum(int[] arr, int lo, int hi) {
        array = arr;
        low = lo;
        high = hi;
    }

    @Override
    protected Long compute() {

        //当任务拆分到小于等于阀值时开始求和
        if (high - low <= SEQUENTIAL_THRESHOLD) {

            long sum = 0;
            for (int i = low; i < high; ++i) {
                sum += array[i];
            }
            return sum;
        } else {  // 任务过大继续拆分
            int mid = low + (high - low) / 2;
            LongSum left = new LongSum(array, low, mid);
            LongSum right = new LongSum(array, mid, high);
            // 提交任务
            left.fork();
            right.fork();
            //获取任务的执行结果return left.join() + right.join();
        }
    }
}

 

posted on 2023-04-03 15:50  zhengbiyu  阅读(25)  评论(0编辑  收藏  举报