并发编程之ForkJoin工作原理分析

目录

1.任务类型

1.1 算法题:如何计算一个大数组的和?

1.2 forkjoin-分治思想计算

1.2.1 fork/join实战代码

 1.2.2 fork/join源码分析

1.2.3 jdk8并行流实战计算


1.任务类型

1. CPU密集型任务

        CPU密集型任务主要是用来发挥CPU的功能,加解密,计算等任务时推荐线程数是CPU核数的1-2倍。

2. IO密集型任务

        IO密集型任务是主要用于读写的任务,文件读写,数据库读写,通信等任务等,一般情况下推荐线程数是CPU核数的很多倍。

        线程数计算方法公式:线程数=CPU核数*(1+平均等待时间/平均工作时间)

        最终的线程数结果需要压测获得,监测JVM的运行情况以及CPU的负载情况。

1.1 算法题:如何计算一个大数组的和?

1.单线程相加

public class SumTest {

    public static void main(String[] args) {
        //定义一个存储1亿个整型数字的数组
        int[] sum = new int[100000000];
        Random random = new Random();
        int temp = 0;
        int result = 0;
        for(int i=0;i < sum.length;i++){
            temp = random.nextInt(100);
            result +=temp;
        }
        System.out.println(result);
    }
}

2. 多线程拆分组合得出最后的值

import java.time.Duration;
import java.time.Instant;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class SumThreadTest {

    private static int SUM = 10000000;//每个任务计算粒度,任务粒度太小会造成计算过慢

    private static int initArrLength = 100000000;//1亿个数组长度

    public static void main(String[] args) throws Exception{
        int result = 0;//最终结果

        //0.定义1亿个长度的数组
        int[] arr = new int[initArrLength];
        Random random = new Random();
        for (int i=0;i < arr.length;i++){
            arr[i] = random.nextInt(100);
        }

        Instant now = Instant.now();
        //1.定义一个固定线程池
        int numThread = arr.length / SUM >0?arr.length / SUM : 1;
        ExecutorService executorService = Executors.newFixedThreadPool(numThread);
        //2.定义一个继承callable接口的任务计算类
        //切分任务提交到线程池中
        Task[] tasks = new Task[numThread];
        Future<Long>[] taskFuture = new Future[numThread];
        for(int j =0 ;j<numThread;j++){
            tasks[j] = new Task(arr, j*SUM,(j+1)*SUM);
            taskFuture[j] = executorService.submit(tasks[j]);
        }
        //3.统计线程池中的线程算出的和得出总和
        for(int n =0;n < numThread;n++){
            result += taskFuture[n].get();
        }
        System.out.println("执行时间:"+ Duration.between(now,Instant.now()).toMillis());
        System.out.println("最终的结果"+result);

        //关闭线程池
        executorService.shutdown();
    }
}

class Task implements Callable<Long>{

    private int[] arr;//存储数组

    private int beginSum = 0;//初始计算粒度

    private int endSum = 0;//初始计算粒度

    public Task(int[] arr,int beginSum,int endSum){
        this.arr = arr;
        this.beginSum = beginSum;
        this.endSum = endSum;
    }

    @Override
    public Long call() throws Exception {
        Long result = 0L;
        //计算数组的值
        for(int i=beginSum;i < endSum;i++){
            result += arr[i];
        }
        return result;
    }
}

1.2 forkjoin-分治思想计算

分治思想:1.分解任务 2.计算任务 3.汇总任务

适合于计算密集型任务处理

 应用场景:

        1. 算法中快速排序,归并排序,二分法查找

        2. 大数据领域框架MapReduce

        3.java提供了Fork/Join框架,实现类叫ForkJoin,重点介绍这个

ForkJoin构造方法核心参数:

        int parallelism 并行线程数

        ForkJoinWorkerThreadFactory factory 工作线程工厂类

        UncaughtExceptionHandler handler 异常处理

        boolean asyncMode(两种模式,FIFO_QUEUE(先进先出) : LIFO_QUEUE(默认,后进先出))

ForkJoinTask是ForkjoinPool的核心之一,它是任务的载体之一,是一个抽象类,里面有两个比较重要的方法:
1. fork() 提交任务
2. join() 获取任务执行结果

通常我们不需要自己实现ForkJoinTask,而只需要根据场景使用以下三个继承的抽象类即可。

1. RecursiveAction 用于递归执行但不需要返回结果的任务

2. RecursiveTask 用于递归执行需要返回结果的任务,通过实现

        protected abstract V compute()

3. CountedCompleter<T> 在任务执行完成时会触发一个自定义的钩子函数,通过实现

public void onCompletion(CountedCompleter<?> caller)方法可以实现

ForkJoin是对传统线程池ThreadPoolExecutor的补充,传统线程池不适合计算密集型任务,不具备任务窃取的能力,这也是与普通线程池的区别之一,也是其性能保证的原因之一。

Forkjoin内有两个对象:线程对象,工作队列(双端队列)。其中的任务操作:push(向队列推送任务),pop(从队列取任务),poll(队列尾部偷取任务)。其中在线程内的队列空闲时,会去繁忙队列中的尾部偷取任务,而繁忙线程内的队列取任务会从头部取,这样是为了减少竞争的可能,加快并行计算效率。

1.2.1 fork/join实战代码

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;

public class LongSum extends RecursiveTask<Long> {

    // 任务拆分最小阈值
    static final int SEQUENTIAL_THRESHOLD = 10000;

    int low;
    int high;
    int[] array;

    public LongSum(int[] arr, int min, int max){
        this.array = arr;
        this.low = min;
        this.high = max;
    }

    @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();
            //获取任务的执行结果,将阻塞当前线程直到对应的子任务完成运行并返回结果
            long rightAns = right.join();
            long leftAns = left.join();
            return leftAns + rightAns;

            //优化:第二种方式
//            left.fork();
//            //获取任务的执行结果,将阻塞当前线程直到对应的子任务完成运行并返回结果
//            long leftAns = left.join();
//            return leftAns + right.compute();
        }
    }
}

class Test2{
    public static void main(String[] args) throws Exception{
        long sum = 0L;
        //100亿个数
        int[] array = new int[100000001];

        for(int i =0;i<100000000;i++){
            array[i] = i;
        }

        long begin = System.currentTimeMillis();
        //递归任务  用于计算数组总和
        LongSum ls = new LongSum(array, 0, array.length);
        // 构建ForkJoinPool
        ForkJoinPool fjp  = new ForkJoinPool();
        //ForkJoin计算数组总和
        ForkJoinTask<Long> submit = fjp.submit(new LongSum(array, 0, array.length));
        System.out.println("计算总数"+submit.get());
        long end = System.currentTimeMillis();
        System.out.println("用时:"+(end-begin)/1000+"秒");
    }
}

 1.2.2 fork/join源码分析

关注点:

1. 线程绑定一个队列

volatile WorkQueue[] workQueues;
final ForkJoinWorkerThreadFactory factory;
DefaultForkJoinWorkerThreadFactory实现类绑定队列

2.提交任务到工作队列

public <T> ForkJoinTask<T> submit(ForkJoinTask<T> task)-->
externalPush(task);
workQueues = new WorkQueue[n];//创建工作队列

看不懂,好像没什么必要看

1.2.3 jdk8并行流实战计算

 使用jdk8特性计算数组性能要比forkjoin要差,而且可能出现活锁现象,其他业务线程干扰计算性能的问题。

import java.util.Random;
import java.util.stream.IntStream;

public class jdk8Test {

    public static void main(String[] args) {
        //定义一个1亿个数字数组
        int[] sumArr = new jdk8Test().getSum(100000000);
        int sum = IntStream.of(sumArr).parallel().sum();
        System.out.println(sum);
    }

    public int[] getSum(int length){
        int[] sum = new int[length];
        Random random = new Random();
        for(int i=0;i<length;i++){
            sum[i] = random.nextInt(100);
        }
        return sum;
    }
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bingtanghulu_6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值