Fork-Join线程池原理

一 ForkJoinPool的执⾏流程

ForkJoinPool和ThreadPoolExecutor的最⼤区别就在于,ForkJoinPool在处理任务时,会触发任务拆分的逻辑,并且将拆分出来的⼦任务继续交给ForkJoinPool中的线程去处理,这样就可能充分利⽤ForkJoinPool中的线程,能更快的去执⾏⼀个⼤任务。
比如:
  • 我们可以自定义一个任务拆分的类SumTask,通过继承RecursiveTask的方式来实现。RecursiveTask为ForkJoinPool的子类,主要方法是compute(),用于任务的计算。
  • SumTask表示⼀个数字累加任务,可以指定begin、end,⽽compute()⽅法就是在执⾏任务,我们可以在compute()⽅法中去做任务拆分的逻辑。
  • 主要逻辑是计算begin~end的累加和:判断begin和end这个范围是否超过100,如果⼩于100则不拆分,直接进⾏累加运算,如果⼤于100,则会把这个任务拆分成两个⼩任务并进⾏fork,并且当前任务要拿到结果,就需要join等这两个⼩任务执⾏完
public class SumTask  extends RecursiveTask<Long> {
    private final int begin;
    private final int end;
    public SumTask(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }
    @Override
    protected Long compute() {
        long sum = 0;
        if (end - begin < 100) {
            for (int i = begin; i <= end; i++) {
                sum += i;
            }
        } else {
            // 拆分逻辑
            int middle = (end + begin) / 2;
            SumTask subtask1 = new SumTask(begin, middle);
            SumTask subtask2 = new SumTask(middle + 1, end);
            subtask1.fork();
            subtask2.fork();
            // 等到⼦任务做完
            long sum1 = subtask1.join();
            long sum2 = subtask2.join();
            sum = sum1 + sum2;
        }
        return sum;
    }
}

二 ForkJoinPool查分任务底层实现分析

        如上图所示:相当于,任务1的执⾏需要等待任务 2 和任务 3 执⾏完,任务 2 和任务 3⼜分别需要等待它们各⾃的⼦任务执⾏完成。
那如果ForkJoinPool执⾏这个任务1 ,共需要多少个线程才能完成呢?
我们来模拟一下流程:
1. 线程 1 执⾏任务 1 ,拆分出任务 2 和任务 3 ,然后阻塞等待任务 2 和任务 3执⾏完成
2. 线程 2 执⾏任务 2 ,拆分出任务 4 和任务 5 ,然后阻塞等待任务 4 和任务 5执⾏完成
3. 线程 3 执⾏任务 3 ,拆分出任务 6 和任务 7 ,然后阻塞等待任务 6 和任务 7执⾏完成
4. 线程 4 执⾏任务4
5. 线程 5 执⾏任务5
6. 线程 6 执⾏任务6
7. 线程 7 执⾏任务7
        按这种思路就需要7 个线程才能完成任务 1 的执⾏,其中 3 个线程负责拆分任务并阻塞等待⼦任务的结果,4个线程负责执⾏最⼩、不⽤拆分的任务。 那这是最合适的⽅案吗,当然不是,如果这个任务1 ,最终拆分出 100 个最⼩的⼦任务,难道真的去开辟 100多个线程来执⾏吗?ForkJoinPool当然不会这么做。
        在创建ForkJoinPool时,可以设置parallelism参数,它就是⽤来指定ForkJoinPool中最⼤线程数的,默认为:Runtime.getRuntime().availableProcessors()。
 public static void main(String[] args) {
        ForkJoinPool forkJoinPool = new ForkJoinPool(10);

        //计算1~10000的累加和
        SumTask sumTask = new SumTask(1,10000);

        ForkJoinTask<Long> submitFuture = forkJoinPool.submit(sumTask);

        //Returns true if this task threw an exception or was cancelled.
        if(submitFuture.isCompletedAbnormally()){
            System.out.println("打印执行的异常信息:"+submitFuture.getException());
        }

        try {
            Long aLong = submitFuture.get();
            System.out.println("打印forkJoin执行结果:"+aLong);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }

    }

三 源码分析

主要的执行逻辑

1、创建队列数组:尝试将给定任务添加到的提交队列中
2、创建一个具体的队列
3、大任务存入队列中
4、新建一个ForkJoinWorkerThread线程,是Thread的子类,主要用于任务的执行。

当执行submit方法提交任务时主要源码逻辑

java.util.concurrent.ForkJoinPool#externalSubmit
Full version of externalPush, handling uncommon cases, as well as performing secondary initialization upon the first submission of the first task to the pool.  It also detects first submission by an external thread and creates a new shared queue if the one at index if empty or contended.
externalPush的完整版本,用于处理不常见的情况,以及在第一个任务首次提交到池时执行二次初始化。它还检测外部线程的首次提交,并在索引处的队列为空或有争用的情况下创建一个新的共享队列。

第一次for(;;)

执行 workQueues = new WorkQueue[n]; 队列的初始化工作, 队列的大小必须是2的整数次幂。 因为我们设置的forkJoinPool的大小是10,他会进行自动的转化。

第二次for(;;)

创建一个队列:q = new WorkQueue(this, null); 然后见这个队列放入到 forkJoinPool的workQueues队列中,执行ws[k] = q;

第三次for(;;)

  • U.putOrderedObject(a, j, task);将task放入q WorkQueue中的某一个位置
  • submitted = true,表示当前的任务可以进行提交执行,执行signalWork(ws, q);方法。意思是Tries to create or activate a worker if too few are active:如果活动的工作线程太少,则尝试创建或激活工作线程
  • java.util.concurrent.ForkJoinPool#signalWork
  • java.util.concurrent.ForkJoinPool#tryAddWorker
  • java.util.concurrent.ForkJoinPool#createWorker:新建一个ForkJoinWorkerThread线程,并且启动执行对应的逻辑调用对应的run()方法
    • ForkJoinWorkerThread的构造方法中建立 线程与工作对列的关系
    •     protected ForkJoinWorkerThread(ForkJoinPool pool) {
              // Use a placeholder until a useful name can be set in registerWorker
              super("aForkJoinWorkerThread");
              this.pool = pool;// 建立与线程池的关系
              this.workQueue = pool.registerWorker(this); //创建一个当前线程自己的工作队列
          }
    • java.util.concurrent.ForkJoinPool#registerWorker:从ForkJoinWorkerThread构造函数回调以建立和记录其工作队列。
    • 这里的核心是run()方法中的java.util.concurrent.ForkJoinPool#runWorker:去当前的线程对应的队列中获取任务,
      • java.util.concurrent.ForkJoinPool.WorkQueue#runTask:执行任务
      • java.util.concurrent.ForkJoinTask#doExec:
      • java.util.concurrent.ForkJoinTask#exec
      • 最终执行到你创建的任务的exec(),我们以RecursiveTask举例也就是会执行到对应的compute()方法
    • compute()方法的一般逻辑就是对任务进行拆分
      • fork()拆分任务
        • java.util.concurrent.ForkJoinPool.WorkQueue#push:将任务push到队列中,仅由非共享队列中的所有者调用。(共享队列版本嵌入在方法externalPush中。)
        • java.util.concurrent.ForkJoinPool#signalWork
        • java.util.concurrent.ForkJoinPool#tryAddWorker
        • java.util.concurrent.ForkJoinPool#createWorker
        • 与第三次开始的逻辑基本一致,拆分子任务的流程也就是:再创建一个线程和他对应的一个工作队列。
      • join()等待子任务完成,进行结果的聚合。
        • java.util.concurrent.ForkJoinTask#doJoin
          • java.util.concurrent.ForkJoinTask#status:表示当前任务的状态,初始值是0,完成之后是负数。
        • java.util.concurrent.ForkJoinPool#awaitJoin:判断当前任务是否执行完成,完成就退出
          • for (;;)不断的自旋判断当前任务是否完成:如果完成就直接退出join逻辑

          • ·java.util.concurrent.ForkJoinPool#helpStealer:任务窃取的核心方法,等待中的线程可以去其他线程中的队列中拿任务进行执行,所以执行速度会非常的快。
          • 那么,⼀个线程在阻塞的过程中会去帮忙执⾏其他队列中的任务,那它如何知道⾃⼰的⼦任务是否执⾏完了呢?java.util.concurrent.ForkJoinPool#helpComplete: ⾃旋操作,每次循环会检查⼀下当前任务是否完成,如果没有完成就会去获取队列中的任务执⾏,执⾏完之后⼜会进⾏⼀次循环并检查当前任务是否完成,完成了就break。

四 执行流程图

默认情况下,ForkJoinPool是非同步模型,就是创建、执行任务以先进后出LIFO的方式处理。

但是ForkJoinPool的任务窃取是以FIFO方式获取task执行。这样的目的是降低并发的概率。

五 总结

1. 可以通过parallelism来设置ForkJoinPool中的线程个数。
2. ForkJoinPool中的每个线程都对应的⼀个任务队列,该队列中存的任务是线程⾃⼰负责的任务,这 些要么⾃⼰执⾏,要么被别的线程执⾏。
3. 每个线程在执⾏任务时都可能拆分出其他⼦任务,这些⼦任务会加⼊到⾃⼰的任务队列中。
4. 每个现在在执⾏任务时,如果拆分出了⼦任务,那么则需要等待这些⼦任务的完成,当前执⾏的任务才能完成,只不过在等待的过程中可以去执⾏其他任务队列中的任务,当前等待的任务⼀旦执⾏完成task.status则会发现改变,这样当前线程就拿到结果了,可以继续执⾏后续代码了,最终把当前任务执⾏完。
  • 14
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值