线程池之ForkJoinPool

概述

ForkJoinPool分支/合并框架,就是在必要的情况下,将一个大任务拆分(fork)成若干个小任务(拆到不能再拆为止),再将一个个的小任务运算的结果进行Join汇总。
线程池之ForkJoinPool_第1张图片

ThreadPool与ForkJoinPool介绍

ThreadPool Executor

一个线程池包括以下四个基本组成部分:

  1. 线程管理器(ThreadPool):用于创建并管理线程池,包括创建线程池,销毁线程池,添加新任务。
  2. 工作线程(PoolWorker):线程池中的线程,在没有任务时处于等待状态,可以循环的执行任务;
  3. 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它注意规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
  4. 任务队列(taskQueue):用于存放没有处理的任务,提供一种缓冲机制。

工作方式

线程池有一个工作队列,队列中包含了要分配给各线程的工作(任务)。当线程空闲时,就会从队列中认领工作。由于线程资源的创建于销毁开销很大,所以ThreadPool允许线程的重用,减少创建于销毁的次数,提供效率。

流程细节

线程池之ForkJoinPool_第2张图片

ForkJoinPool Executor

ForkJoinPool组成类

  1. ForkJoinPool:充当fork/join框架里面的管理者,最原始的任务都要交给它才能处理。它负责控制整个fork/join有多少workerThread;workerThread的创建,激活都是由它来掌控的。它还负责workQueue队列的创建和分配;每当创建一个workerThread,它负责分配相应的workQueue,然后它把接到的活都交给workerThread去处理,它可以说是整个fork/join的容器。
  2. ForkJoinWorkerThread:fork/join里面真正干活的工人,本质是一个线程。里面有一个ForkJoinPool.workQueue的队列存放这它要干的活,在开始接活之前它要向ForkJoinPool注册(registerWorker),拿到相应的workQueue。然后就从workQueue里面拿任务来处理。他是依附于ForkJoinPool而存活,如果ForkJoinPool销毁了,它也会跟着结束。
  3. ForkJoinPool.workQueue:双端队列就是它,它负责存储接收的任务。
  4. ForkJoinTask:代表fork/join里面的任务类型,我们一般用它的两个子类RecursiveTask、RecursiveAction。这两个区别在于RecursiveTask任务有返回值,RecursiveAction没有返回值。任务的处理逻辑包括任务的切分都集中在compute()方法里面。

工作方式

使用一直分治算法,递归地将任务分割成更小的子任务,其中阈值可配置,然后把子任务分配给不同的线程并发执行,最后再把结果组合起来。该方法常见于数组于集合的运算。
由于提交的任务不一定能够递归的分割成ForkJoinTask,且ForkJoinTask执行时间不等长,所以ForkJoinPool使用一种工作窃取的算法,允许空闲的线程“窃取”分配给另一线程的工作,由于工作无法平均分配并执行,所以工作窃取算法能更高效地利用硬件资源。

流程细节

线程池之ForkJoinPool_第3张图片

Fork/Join框架与线程池的区别

  1. ForkJoinPool不是为了替代ExecutorService,而是它的补充,在某些场景下性能比ExecutorService更好。
  2. ForkJoinPool主要用于实现“分而治之”的算法,特别是分治之后递归调用的函数,例如quick sort等。
  3. ForkJoinPool最适合的是计算密集型的任务,如果存在I/O,线程同步,sleep()等会造成线程长时间阻塞的情况,最好配合使用ManagedBlocker。
  4. 采用“工作窃取”模式(work-stealing)
    当执行新的任务时它可以将其拆分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中,本质上是为了保证线程不闲着,保持所有cpu繁忙。
  5. 与一般线程池处理任务的方式不同。
    相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上。在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程就会处于等待状态(每个线程处理任务是同步的)。而在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行,那么处理该子问题的线程(等待其实就是空闲)会主动寻找尚未运行的子问题来执行(空闲的线程会从一个双端队列的尾部窃取一个任务来执行,不会让自己闲着)。这种方式减少了线程的等待时间,提高了性能。

JDK8 对 Fork/Join 的优化

JDK8 对 Fork/Join 的优化:主要是让 Fork/Join 使用起来更加方便。对 Fork/Join 进行了封装,简化使用方式。

应用场景

  • ThreadPool:多见于线程并发,阻塞时延比较长的,这种线程池比较常用,一般设置的线程个数根据业务性能要求会比较多。
  • ForkJoinPool:特点是少量线程完成大量任务,一般用于非阻塞的,能快速处理的业务,或阻塞时延比较低的。

使用示例

package com.study.practice;

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
import java.util.stream.LongStream;

/**
 * @Description : test
 * @Version : V1.0.0
 * @Date : 2022/4/3 11:35
 */
public class Test {

    public static void main(String[] args) {
        // jdk7的写法
        long startTime = System.currentTimeMillis();
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinCalculate forkJoinCalculate = new ForkJoinCalculate(0, 1000000000L);
        Long res = forkJoinPool.invoke(forkJoinCalculate);
        System.out.println("jdk7 result " + res + " cost " + (System.currentTimeMillis() - startTime));

        // jdk8的写法
        long start = System.currentTimeMillis();
        // 只有并行流才会使用fork/join框架,否则就是单线程执行
        sum = LongStream.rangeClosed(0, 1000000000L).parallel().sum();
        System.out.println("jdk8 parallel exe result " + sum + " cost " + (System.currentTimeMillis() - start));
    }

    private static class ForkJoinCalculate extends RecursiveTask<Long> {

        private long start;

        private long end;

        private static final long THRESHOLD = 10000;

        public ForkJoinCalculate(long start, long end) {
            this.start = start;
            this.end = end;
        }

        @Override
        protected Long compute() {
            if (end - start <= THRESHOLD) {
                long sum = 0;
                for (long i = start; i <= end; i++) {
                    sum += i;
                }
                return sum;
            } else {
                long middle = (start + end) / 2;
                ForkJoinCalculate leftFork = new ForkJoinCalculate(start, middle);
                // 把任务分配到异步线程池中
                leftFork.fork();
                ForkJoinCalculate rightFork = new ForkJoinCalculate(middle + 1, end);
                // 把任务分配到异步线程池中
                rightFork.fork();
                // 把结果合并
                return leftFork.join() + rightFork.join();
            }
        }
    }
}

执行结果:

jdk7 result 500000000500000000 cost 2438
jdk8 parallel exe result 500000000500000000 cost 93

参考

线程池之ThreadPool与ForkJoinPool
Java-ForkJoinPool详解
【小家java】Java线程池之—ForkJoinPool线程池的使用以及原理

你可能感兴趣的:(并发编程,JAVA,java,后端)