Fork Join 体现了分而治之

什么是分而治之?

  规模为N的问题,如果N<阈值,直接解决,N>阈值,将N分解为K个小规模子问题,子问题互相对立,与原问题形式相同,将子问题的解合并得到原问题的解

Fork Join 框架:

  就是在必要的情况下,将一个大任务,进行拆分(fork)成若干了小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行join汇总

Fork Join的另一大特点:工作密取

什么是工作密取?

  就是在按指定阈值拆分后,的多个线程,如果线程A的任务执行的比较快,获得到的CPU时间片比较多,那么在他执行完毕后,就会从未执行完毕的线程的任务中的尾部,进行任务窃取,任务完成后再把结果放回去,不会造成任务竞争,因为自身执行线程的任务是从头部开始获取的,而空闲的线程是从尾部窃取的.

Fork Join使用的标准范式

在使用的过程中我们是无法直接new 一个ForkJoinTask类的,他是一个抽象类,但是他提供了两个子类,RecursiveTask和ResursiveAction两个子抽象类.我们使用的时候,如果需要有返回值,我们就继承RecursiveTask,如果不需要返回值我们就继承RecursiveAction

Fork Join实战

  Fork Join的同步用法同时演示返回结果值:统计整数数组中所有元素的和

先创建一个工具类用于制作整数数组

package org.dance.day2.forkjoin.sum;

import java.util.Random;

/**
* 数组制作类
* @author ZYGisComputer
*/
public class MarkArray { public static final int ARRAY_LENGTH = 4000; /**
* int数组生成器
* @return int数组
*/
public static int[] markArray(){ Random random = new Random(); int[] array = new int[ARRAY_LENGTH]; for (int i = 0; i < ARRAY_LENGTH; i++) {
array[i] = random.nextInt(ARRAY_LENGTH*3);
} return array; } }

然后创建一个单线程的求和类,用于和多线程的对比

package org.dance.day2.forkjoin.sum;

import org.dance.tools.SleepTools;

/**
* 单线程实现求和
* @author ZYGisComputer
*/
public class SumNormal { public static void main(String[] args) {
int count = 0; // 获取数组
int[] src = MarkArray.markArray(); long l = System.currentTimeMillis(); for (int i = 0; i < src.length; i++) {
// 执行一毫秒的休眠
SleepTools.ms(1);
count += src[i];
} System.out.println("The count is "+count+" spend time "+(System.currentTimeMillis() - l));
} }

使用继承RecursiveTask的ForkJoin框架类,完成多线程的求和计算

package org.dance.day2.forkjoin.sum;

import org.dance.tools.SleepTools;

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask; /**
* 使用ForkJoin框架实现求和
* @author ZYGisComputer
*/
public class SumArray { /**
* 因为需要返回值所以继承RecursiveTask类
* 因为计算的是整型,所以泛型是Integer
*/
private static class SumTask extends RecursiveTask<Integer> { // 计算阈值
private final static int THRESHOLD = MarkArray.ARRAY_LENGTH/10; // 源数组
private int[] src; // 开始坐标
private int fromIndex; // 结束坐标
private int toIndex; /**
* 通过创建时传入
* @param src 元素组
* @param fromIndex 开始坐标
* @param toIndex 结束坐标
*/
public SumTask(int[] src, int fromIndex, int toIndex) {
this.src = src;
this.fromIndex = fromIndex;
this.toIndex = toIndex;
} /**
* 覆盖执行方法
* @return 整型
*/
@Override
protected Integer compute() {
// 如果 结束下标减去开始下标小于阈值的时候,那么任务就可以开始执行了
if( toIndex - fromIndex < THRESHOLD ){
int count = 0;
// 从开始下标开始循环,循环到结束下标
for (int i = fromIndex; i < toIndex; i++) {
// 休眠1毫秒
SleepTools.ms(1);
count += src[i];
}
return count;
}else{
// 大于阈值 继续拆分任务
// 从formIndex---------------------->到toIndex
// 计算中间值,从formIndex----------计算mid------------>到toIndex
int mid = (fromIndex + toIndex) / 2;
// 左侧任务 从formIndex------------>到mid结束
SumTask left = new SumTask(src, fromIndex, mid);
// 右侧任务 从mid+1开始------------->到toIndex结束
SumTask right = new SumTask(src, mid+1,toIndex);
// 调用任务
invokeAll(left,right);
// 获取结果
return left.join() + right.join();
}
}
} public static void main(String[] args) { // 创建ForkJoin任务池
ForkJoinPool forkJoinPool = new ForkJoinPool(); // 制作源数组
int[] src = MarkArray.markArray(); long l = System.currentTimeMillis(); // 创建一个任务 下标因为从0开始所以结束下标需要-1
SumTask sumTask = new SumTask(src, 0, src.length - 1); // 提交同步任务
Integer invoke = forkJoinPool.invoke(sumTask); // 无论是接收invoke方法的返回值还是调用任务的Join方法都可以获取到结果值
System.out.println("The count is "+invoke+" spend time "+(System.currentTimeMillis() - l));
System.out.println("The count is "+sumTask.join()+" spend time "+(System.currentTimeMillis() - l)); } }

运行结果对比:

现在是4000大小的数组,每次循环休眠1毫秒

单线程执行的结果:

The count is 23751855 spend time 5395

多线程执行的结果:

The count is 23387745 spend time 1487
The count is 23387745 spend time 1487

结果对比多线程比单线程快大概3倍的时间

接下来我们去掉休眠时间,再次进行结果对比:

单线程执行结果:

The count is 23460518 spend time 0

多线程执行结果:

The count is 24078313 spend time 3
The count is 24078313 spend time 3

然后我们惊奇的发现,多线程比单线程还要慢,为什么呢,是因为在小数据量的情况下,单线程,执行期间没有花费上下文切换时间,多线程执行期间是需要花费线程之间上下文切换的时间的,每次上下文切换时间之前说过,大概花费20000-50000个时钟周期的,所以3ms大概切换了10次左右,因为我们是4000大小的数组,阈值是除以10,所以启动的应该是10个线程,所以多线程执行会比单线程慢一些,所以说我们在用多线程的时候,就需要考虑线程之间的上下文切花问题,并不一定多线程就一定是好,我们只是看需求,而选择,就像Redis一样设计的时候就是单线程的,但是他的强大,却是比多线程的memcached更加强大,所以说没有肯定的结论,只有适合和不适合.

接下来我们往大调整整型数组的大小

4000调整为1亿,然后对比结果

单线程执行结果:

The count is -331253431 spend time 51

多线程执行结果:

The count is 75277814 spend time 49
The count is 75277814 spend time 50

我们可以发现,所用的执行时间,已经大概一致了

继续调大1亿调整为3亿,继续对比结果

单线程执行结果:

The count is 57724808 spend time 205

多线程执行结果:

The count is 1028352167 spend time 106
The count is 1028352167 spend time 106

现在单线程已经是多线程的执行时间的两倍了,由此可见,当数据量越来越大的时候,单线程的性能往往就会逐渐降低,而多线程的优势就渐渐体现出来了

所谓的同步用法就是在调用

forkJoinPool.invoke(sumTask);

之后主线程就在这里阻塞了,需要等待,执行完成后,主线程才能继续往下执行,接下里我们看异步用法

  Fork Join的异步用法同时演示不要求返回值:遍历指定目录(含子目录)寻找指定类型文件

package org.dance.day2.forkjoin;

import org.dance.tools.SleepTools;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction; /**
* 使用ForkJoin框架实现不定个数的任务执行
* @author ZYGisComputer
*/
public class FindDirsFiles { /**
* 因为搜索文件不需要返回值,所以我们继承RecursiveAction
*/
private static class FindFilesByDirs extends RecursiveAction{ private File path; public FindFilesByDirs(File path) {
this.path = path;
} @Override
protected void compute() { // 创建任务容器
List<FindFilesByDirs> findFilesByDirs = new ArrayList<>(); // 获取文件夹下所有的对象
File[] files = path.listFiles(); if(null!=files){ for (File file : files) {
// 判断是否是文件夹
if (file.isDirectory()){
// 添加到任务容器中
findFilesByDirs.add(new FindFilesByDirs(file));
}else{
// 如果是一个文件,那么检查这个文件是否符合需求
if(file.getAbsolutePath().endsWith(".txt")){
// 如果符合 打印
System.out.println("文件:"+file.getAbsolutePath());
}
}
} // 判断任务容器是否为空
if(!findFilesByDirs.isEmpty()){
// 递交任务组
for (FindFilesByDirs filesByDirs : invokeAll(findFilesByDirs)) {
// 等待子任务执行完成
filesByDirs.join();
} } } }
} public static void main(String[] args) { // 创建ForkJoin池
ForkJoinPool forkJoinPool = new ForkJoinPool(); File path = new File("E:/"); // 创建任务
FindFilesByDirs findFilesByDirs = new FindFilesByDirs(path); // 异步调用 这个方法是没有返回值的
forkJoinPool.execute(findFilesByDirs); System.out.println("Task is Running................");
SleepTools.ms(1); // 在这里做这个只是测试ForkJoin是否为异步,当执行ForkJoin的时候主线程是否继续执行
int otherWork = 0;
for (int i = 0; i < 100; i++) {
otherWork += i;
}
System.out.println("Main thread done sth.......,otherWork:"+otherWork); // 如果是有返回值的话,可以获取,当然这个join方法是一个阻塞式的,因为主线程执行的太快了,ForkJoin还没执行完成主线程就死亡了,所以在这里调用一下阻塞,等待ForkJoin执行完成
findFilesByDirs.join(); System.out.println("Thread end!"); } }

执行结果:

Task is Running................
Main thread done sth.......,otherWork:4950
文件:E:\dance\activiti-ruoyi\RuoYi-Process\ruoyi-admin\src\main\resources\static\file\rml.txt
文件:E:\dance\activiti-ruoyi\RuoYi-Process\ruoyi-admin\target\classes\banner.txt
文件:E:\dance\activiti-ruoyi\RuoYi-Process\ruoyi-admin\target\classes\static\ajax\libs\jquery-ztree\3.5\log v3.x.txt
文件:E:\dance\activiti-ruoyi\RuoYi-Process\ruoyi-admin\target\classes\static\file\rml.txt
........................
Thread end!

从执行结果中可以看到,主线程的执行时在ForkJoin执行之前就执行了,但是代码中却是在ForkJoin执行之后执行的,所以说这是异步的,线程是并行执行的,异步执行只能通过调用任务线程的Join方法获取返回值,execute方法是没有返回值的

作者:彼岸舞

时间:2020\09\18

内容关于:并发编程

本文来源于网络,只做技术分享,一概不负任何责任

Fork Join 并发任务执行框架的更多相关文章

  1. fork/join并发编程

    Fork & Join 的具体含义 Fork 一词的原始含义是吃饭用的叉子,也有分叉的意思.在Linux 平台中,函数 fork()用来创建子进程,使得系统进程可以多一个执行分支.在 Java ...

  2. 谈谈fork/join实现原理

    害,又是一个炒冷饭的时间.fork/join是在jdk1.7中出现的一个并发工作包,其特点是可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出.从而达到多 ...

  3. fork/join 全面剖析

    fork/join作为一个并发框架在jdk7的时候就加入到了我们的java并发包java.util.concurrent中,并且在java 8 的lambda并行流中充当着底层框架的角色.这样一个优秀 ...

  4. FutureTask、Fork/Join、 BlockingQueue

    我们之前学习创建线程有Thread和Runnable两种方式,但是两种方式都无法获得执行的结果. 而Callable和Future在任务完成后得到结果.   Future是一个接口,表示一个任务的周期 ...

  5. 转:聊聊并发(八)——Fork/Join框架介绍

    1. 什么是Fork/Join框架 Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架. 我们再通过 ...

  6. 并发编程学习笔记(12)----Fork/Join框架

    1. Fork/Join 的概念 Fork指的是将系统进程分成多个执行分支(线程),Join即是等待,当fork()方法创建了多个线程之后,需要等待这些分支执行完毕之后,才能得到最终的结果,因此joi ...

  7. Java 并发编程 -- Fork/Join 框架

    概述 Fork/Join 框架是 Java7 提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架.下图是网上流传的 Fork Join 的 ...

  8. Java并发编程(07):Fork/Join框架机制详解

    本文源码:GitHub·点这里 || GitEE·点这里 一.Fork/Join框架 Java提供Fork/Join框架用于并行执行任务,核心的思想就是将一个大任务切分成多个小任务,然后汇总每个小任务 ...

  9. ☕【Java技术指南】「并发编程专题」Fork/Join框架基本使用和原理探究(基础篇)

    前提概述 Java 7开始引入了一种新的Fork/Join线程池,它可以执行一种特殊的任务:把一个大任务拆成多个小任务并行执行. 我们举个例子:如果要计算一个超大数组的和,最简单的做法是用一个循环在一 ...

随机推荐

  1. (转)C# 获取当前路径的7中方法

    //获取模块的完整路径. string path1 = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName; //获取 ...

  2. 【Apollo】(2)--- Apollo架构设计

    Apollo架构设计 上一篇博客有讲到:[Apollo](1)--- Apollo入门介绍篇 这篇来写Apollo的核心架构设计 一.整体架构 Apollo整体架构图,已由作者宋顺已经给出: 这幅图所 ...

  3. 团队作业4:第七篇Scrum冲刺博客(歪瑞古德小队)

    目录 一.Daily Scrum Meeting 1.1 会议照片 1.2 项目进展 二.项目燃尽图 三.签入记录 3.1 代码/文档签入记录 3.2 Code Review 记录 3.3 issue ...

  4. 常见面试题之操作系统中的LRU缓存机制实现

    LRU缓存机制,全称Least Recently Used,字面意思就是最近最少使用,是一种缓存淘汰策略.换句话说,LRU机制就是认为最近使用的数据是有用的,很久没用过的数据是无用的,当内存满了就优先 ...

  5. 实用教程!SPSSAU验证性因子分析思路总结

    验证性因子分析,是用于测量因子与测量项(量表题项)之间的对应关系是否与研究者预测保持一致的一种研究方法.尽管因子分析适合任何学科使用,但以社会科学居多. 目前有很多软件都可以非常便利地实现验证性因子分 ...

  6. Java算法——分治法

         一.基本概念 在计算机科学中,分治法是一种很重要的算法.字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简 ...

  7. 系综的实现方式(nve、nvt、npt)

    一.NVE系综 更新位置和速度,v和e恒定. 二.NVT系综 几种实现的方式: 如: fix 1 all nve #更新位置和速度,e和V保持不变.若只有这个命令,就只nve系综,如果和控温命令一起, ...

  8. 终于弄明白了 Singleton,Transient,Scoped 的作用域是如何实现的

    一:背景 1. 讲故事 前几天有位朋友让我有时间分析一下 aspnetcore 中为什么向 ServiceCollection 中注入的 Class 可以做到 Singleton,Transient, ...

  9. 善用Bash history 命令

    大家好,我是良许 相信大家平时都有用 history 命令来查看命令历史记录,但是实际上 history 命令并非只有这个功能,history 还有很多有用的功能.尤其是 Bash 版本的 histo ...

  10. 在Spring中拦截器的使用

    Filter Filter是Servlet容器实现的,并不是由Spring 实现的 下面是一个例子 import java.io.IOException; import javax.servlet.F ...