forkjoin和forkjoinpool讲解

本文介绍了Java中的ForkJoinPool和工作窃取算法,阐述了为何需要ForkJoin,包括它如何节省资源和均衡利用CPU。通过代码示例和工作原理解析,详细讲解了ForkJoinPool的主要类如ForkJoinTask、RecursiveAction和RecursiveTask,以及关键方法如fork()和join()。最后,解释了ForkJoinPool的整体工作流程和工作队列特性。

背景

 对于java开发从业人员来说,并发编程是绕不开的话题,juc并发包下提供了一系列多线程场景解决方案。
 随着jdk1.8的普及,多线程处理问题,除了使用使用线程池(ExecutorService),很多人选择了parallelStream() 并行流,底层使用forkjoin实现并行处理。
 那么并行和并发又有什么区别?究竟改如何选择?滥用时又会有什么影响?
  这些问题我分以下几篇文章进行详细说明:
  1. 多线程并发和并行的区别
  2. parallelStream()并行滥用的后果
  3. forkjoin和forkjoinpool讲解 (本文)
  4. 线程池正确用法

为什么需要forkjoin

两个好处ThreadPool无法替代
- 线程私有队列,更节省资源
- 工作窃取机制,均衡利用cpu
用下面这个例子说明以上两点,如果将一段计算拆分成如下16段进行多线程计算。

  1. 如果用多线程递归拆分,需要16个线程,而且当cpu核数只有4核时,会存在cpu切换的额外消耗;用forkjoin默认使用cpu相同核数的线程,将任务放入队列进行并行计算。
  2. 如果使用多线程计算,如果某个线程分到的任务执行比较慢,其他线程先执行完,就会有额外的cpu浪费。如果用forkjoin,先执行完的线程,会到慢的线程队列中窃取任务,均匀利用cpu。
    在这里插入图片描述

先看代码

看原理之前,先简单的看forkjoin例子

package com.chainup.forkjoin;
import java.util.concurrent.*;
/**
 * @Author zhongjingyun
 * 通过forkjoin完成1~1000累加
 * @Date 2020/9/8 下午11:18
 **/
public class ForkJoinDemo {
    public static void main(String[] args) throws Exception{
        //使用ForkJoinPool来执行任务
        ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
        //生成一个计算任务,负责计算1+2+3+4
        CountTaskTmp task = new CountTaskTmp(1, 1000);
        long r = forkJoinPool.invoke(task);
        System.out.println(r);

    }
}

class CountTaskTmp extends RecursiveTask<Long> {
    private static final long THRESHOLD = 10;
    private long start;
    private long end;

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

    //实现compute 方法来实现任务切分和计算
    @Override
    protected Long compute() {
        System.out.println("当前线程开始:" + Thread.currentThread().getName() + " start : " + start + " end : " + end);
        long sum = 0;
        boolean canCompute = (end - start) <= THRESHOLD;
        if (canCompute) {
            for (long i = start; i <= end; i++){
                sum += i;
            }
        } else {
            //如果任务大于阀值,就分裂成两个子任务计算
            long mid = (start + end) / 2;

            CountTaskTmp leftTask = new CountTaskTmp(start, mid);
            CountTaskTmp rightTask = new CountTaskTmp(mid + 1, end);

            //执行子任务
            leftTask.fork();
            rightTask.fork();
            //等待子任务执行完,并得到结果
            long leftResult = leftTask.join();
            long rightResult = rightTask.join();
            sum = leftResult + rightResult;
        }
        System.out.println("当前线程结束:" + Thread.currentThread().getName() + " start : " + start + " end : " + end);
        return sum;
    }
}

结合多线程理解forkjoin代码

forkjoin主要的类

  • ForkJoinPool:实现ForkJoin的线程池,类似于ThreadPool
  • ForkJoinWorkerThread 实现ForkJoin的线程,继承了Thread
  • ForkJoinTask 一个描述ForkJoin的抽象类,类似Runnable/Callable
    • RecursiveAction 无返回结果的ForkJoinTask实现,类似Runnable
    • RecursiveTask 有返回结果的ForkJoinTask实现,类似Callable
    • CountedCompleter 在任务完成执行后会触发执行一个自定义的钩子函数

forkjoin常用方法

  • fork():类似于Thread.start(),但是它并不立即执行任务,而是将任务放入工作队列中
  • join():跟Thread.join()不同,ForkJoinTask的join()方法并不简单的阻塞线程
    利用工作线程运行其他任务
    当一个工作线程中调用join(),它将处理其他任务,直到注意到目标子任务已经完成

forkjoin设计原理

forkjoin整体工作流程

在这里插入图片描述

工作队列和线程池

  • ForkJoinPool工作线程池
    和线程池一样继承了AbstractExecutorService,专门用来执行ForkJoinTask。提供公共的静态线程池:ForkJoinPool.commonPool();
    也可以单独创建指定线程池大小:new ForkJoinPool();默认大小为cpu核数:Runtime.getRuntime().availableProcessors()

  • WorkQueue
    所有的工作线程均有一个自己的工作队列WorkQueue,具备以下特征

    • 双端队列(Deque)
    • 从队头取任务
    • 线程私有,不共享
  • 工作窃取机制
    为了最大化CPU利用率,空闲的线程将从其他线程的队列中“窃取”任务来执行
    在这里插入图片描述
    在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值