线程池ForkJoinPool详解

由一道算法题引发的思考

算法题:如何充分利用多核CPU的性能,快速对一个2千万大小的数组进行排序?

这道算法题可以拆解来看:

1)首先这是一道排序的算法题,而且是需要使用高效的排序算法对2千万大小的数组进行排序,可以考虑使用快速排序或者归并排序。

2)可以使用多线程并行排序算法来充分利用多核CPU的性能。

基于归并排序算法实现

快速对一个大小为2千万的数组进行排序,可以使用高效的归并排序算法来实现。

什么是归并排序

归并排序(Merge Sort)是一种基于分治思想的排序算法。归并排序的基本思想是将一个大数组分成两个相等大小的子数组,对每个子数组分别进行排序,然后将两个子数组合并成一个有序的大数组。因为常常使用递归实现(由先拆分后合并的性质决定的),所以我们称其为归并排序。

归并排序的步骤包括以下几个方面:

  • 将数组分成两个子数组
  • 对每个子数组进行排序
  • 合并两个有序的子数组

归并排序的时间复杂度为O(nlogn),空间复杂度为O(n),其中n为数组的长度。

分治思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。

分治思想的步骤如下:

分解:将要解决的问题划分成若干规模较小的同类问题;

求解:当子问题划分得足够小时,用较简单的方法解决;

合并:按原问题的要求,将子问题的解逐层合并构成原问题的解。

计算机十大经典算法中的归并排序、快速排序、二分查找都是基于分治思想实现的算法。

分治任务模型图如下:

归并排序演示地址:

https://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html

使用归并排序实现上面的算法题

单线程实现归并排序

单线程归并算法的实现,它的基本思路是将序列分成两个部分,分别进行递归排序,然后将排序好的子序列合并起来。

Fork/Join并行归并排序

并行归并排序是一种利用多线程实现的归并排序算法。它的基本思路是将数据分成若干部分,然后在不同线程上对这些部分进行归并排序,最后将排好序的部分合并成有序数组。在多核CPU上,这种算法也能够有效提高排序速度。

可以使用Java的Fork/Join框架来实现归并排序的并行化

代码示例

import java.util.Arrays;
import java.util.concurrent.RecursiveAction;

/**
 * 利用fork-join实现数组排序
 */
public class MergeSortTask extends RecursiveAction {

  // 数组是否继续拆分的阈值,数组长度低于此阈值就不再进行拆分
  private final int threshold;
  // 要排序的数组
  private int[] arrayToSort;

  public MergeSortTask(final int[] arrayToSort, final int threshold) {
    this.arrayToSort = arrayToSort;
    this.threshold = threshold;
  }

  @Override
  protected void compute() {
    // 拆分后的数组长度小于阈值,直接进行排序
    if (arrayToSort.length <= threshold) {
      // 调用jdk提供的排序方法
      Arrays.sort(arrayToSort);
      return;
    }
    // 对数组进行拆分
    int midpoint = arrayToSort.length / 2;
    int[] leftArray = Arrays.copyOfRange(arrayToSort, 0, midpoint);
    int[] rightArray = Arrays.copyOfRange(arrayToSort, midpoint, arrayToSort.length);
    MergeSortTask leftTask = new MergeSortTask(leftArray, threshold);
    MergeSortTask rightTask = new MergeSortTask(rightArray, threshold);
    //提交任务
//    leftTask.fork();
//    rightTask.fork();
//    // 阻塞当前线程,直到获取任务的执行结果
//    leftTask.join();
//    rightTask.join();
    // 调用任务,阻塞当前线程,直到所有子任务执行完成
    invokeAll(leftTask, rightTask);
    // 合并排序结果
    arrayToSort = MergeSort.merge(leftTask.getSortedArray(), rightTask.getSortedArray());
  }

  public int[] getSortedArray() {
    return arrayToSort;
  }
}
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.ForkJoinPool;

public class MergeSort {

  // 要排序的数组
  private final int[] arrayToSort;
  // 拆分的阈值,低于此阈值就不再进行拆分
  private final int threshold;

  public MergeSort(final int[] arrayToSort, final int threshold) {
    this.arrayToSort = arrayToSort;
    this.threshold = threshold;
  }

  /**
   * 排序
   *
   * @return
   */
  public int[] mergeSort() {
    return mergeSort(arrayToSort, threshold);
  }

  public static int[] mergeSort(final int[] arrayToSort, int threshold) {
    // 拆分后的数组长度小于阈值,直接进行排序
    if (arrayToSort.length <= threshold) {
      // 调用jdk提供的排序方法
      Arrays.sort(arrayToSort);
      return arrayToSort;
    }
    int midpoint = arrayToSort.length / 2;
    // 对数组进行拆分
    int[] leftArray = Arrays.copyOfRange(arrayToSort, 0, midpoint);
    int[] rightArray = Arrays.copyOfRange(arrayToSort, midpoint, arrayToSort.length);
    // 递归调用
    leftArray = mergeSort(leftArray, threshold);
    rightArray = mergeSort(rightArray, threshold);
    // 合并排序结果
    return merge(leftArray, rightArray);
  }

  public static int[] merge(final int[] leftArray, final int[] rightArray) {
    // 定义用于合并结果的数组
    int[] mergedArray = new int[leftArray.length + rightArray.length];
    int mergedArrayPos = 0;
    int leftArrayPos = 0;
    int rightArrayPos = 0;
    while (leftArrayPos < leftArray.length && rightArrayPos < rightArray.length) {
      if (leftArray[leftArrayPos] <= rightArray[rightArrayPos]) {
        mergedArray[mergedArrayPos] = leftArray[leftArrayPos];
        leftArrayPos++;
      } else {
        mergedArray[mergedArrayPos] = rightArray[rightArrayPos];
        rightArrayPos++;
      }
      mergedArrayPos++;
    }
    while (leftArrayPos < leftArray.length) {
      mergedArray[mergedArrayPos] = leftArray[leftArrayPos];
      leftArrayPos++;
      mergedArrayPos++;
    }
    while (rightArrayPos < rightArray.length) {
      mergedArray[mergedArrayPos] = rightArray[rightArrayPos];
      rightArrayPos++;
      mergedArrayPos++;
    }
    return mergedArray;
  }

  /**
   * 随机生成数组
   *
   * @param size 数组的大小
   * @return
   */
  public static int[] buildRandomIntArray(final int size) {
    int[] arrayToCalculateSumOf = new int[size];
    Random generator = new Random();
    for (int i = 0; i < arrayToCalculateSumOf.length; i++) {
      arrayToCalculateSumOf[i] = generator.nextInt(100000000);
    }
    return arrayToCalculateSumOf;
  }

  public static void main(String[] args) {
//    int[] arrayToSortByMergeSort = {6, 5, 3, 1, 8, 7, 2, 4};
//    int threshold = 2;
    int[] arrayToSortByMergeSort = buildRandomIntArray(20000000);
    int threshold = 10000;
    MergeSort mergeSort = new MergeSort(arrayToSortByMergeSort, threshold);
    long startTime = System.nanoTime();
    int[] sort = mergeSort.mergeSort();
    long duration = System.nanoTime() - startTime;
//    System.out.println("执行结果:" + Arrays.toString(sort) + ",单线程归并排序时间:" + (duration / (1000f * 1000f)) + "毫秒");
    System.out.println("单线程归并排序时间:" + (duration / (1000f * 1000f)) + "毫秒");

    // 生成测试数组  用于ForkJoin排序
    int[] arrayToSortByForkJoin = Arrays.copyOf(arrayToSortByMergeSort, arrayToSortByMergeSort.length);
    // 获取处理器数量,用于配置ForkJoin线程池中的核心线程数
    int processors = Runtime.getRuntime().availableProcessors();
    // 利用ForkJoin排序
    MergeSortTask mergeSortTask = new MergeSortTask(arrayToSortByForkJoin, threshold);
    // 构建ForkJoin线程池,传入核心线程数
    ForkJoinPool forkJoinPool = new ForkJoinPool(processors);
    startTime = System.nanoTime();
    // 执行排序任务
    forkJoinPool.invoke(mergeSortTask);
    duration = System.nanoTime() - startTime;
    System.out.println("ForkJoin排序时间: " + (duration / (1000f * 1000f)) + "毫秒");
  }
}  

执行结果对比

单线程归并排序时间:3302.956毫秒
ForkJoin排序时间: 1592.1223毫秒

根据测试结果可以看出,数组越大,利用Fork/Join框架实现的并行化归并排序比单线程归并排序的效率更高。 

在这个示例中,使用Fork/Join框架实现了归并排序算法,并通过递归调用实现了并行化。使用Fork/Join框架实现归并排序算法的关键在于将排序任务分解成小的任务,使用Fork/Join框架将这些小任务提交给线程池中的不同线程并行执行,并在最后将排序后的结果进行合并。这样可以充分利用多核CPU的并行处理能力,提高程序的执行效率。 

并行实现归并排序的优化和注意事项

在实际应用中,我们需要考虑数据分布的均匀性、内存使用情况、线程切换开销等因素,以充分利用多核CPU并保证算法的正确性和效率。

以下是并行实现归并排序的一些优化和注意事项:

  • 任务的大小:任务大小的选择会影响并行算法的效率和负载均衡,如果任务太小,会造成任务划分和合并的开销过大;如果任务太大,会导致任务无法充分利用多核CPU并行处理能力。因此,在实际应用中需要根据数据量、CPU核心数等因素选择合适的任务大小。
  • 负载均衡:并行算法需要保证负载均衡,即各个线程执行的任务大小和时间应该尽可能相等,否则会导致某些线程负载过重,而其他线程负载过轻的情况。在归并排序中,可以通过递归调用实现负载均衡,但是需要注意递归的层数不能太深,否则会导致任务划分和合并的开销过大。
  • 数据分布:数据分布的均匀性也会影响并行算法的效率和负载均衡。在归并排序中,如果数据分布不均匀,会导致某些线程处理的数据量过大,而其他线程处理的数据量过小的情况。因此,在实际应用中需要考虑数据的分布情况,尽可能将数据分成大小相等的子数组。
  • 内存使用:并行算法需要考虑内存的使用情况,特别是在处理大规模数据时,内存的使用情况会对算法的执行效率产生重要影响。在归并排序中,可以通过对数据进行原地归并实现内存的节约,但是需要注意归并的实现方式,以避免数据的覆盖和不稳定排序等问题。
  • 线程切换:线程切换是并行算法的一个重要开销,需要尽量减少线程的切换次数,以提高算法的执行效率。在归并排序中,可以通过设置线程池的大小和调整任务大小等方式控制线程的数量和切换开销,以实现算法的最优性能。

Fork/Join框架介绍

什么是Fork/Join

Fork/Join是一个是一个并行计算的框架,主要就是用来支持分治任务模型的,这个计算框架里的 Fork 对应的是分治任务模型里的任务分解,Join 对应的是结果合并。它的核心思想是将一个大任务分成许多小任务,然后并行执行这些小任务,最终将它们的结果合并成一个大的结果。它适用于可以采用分治策略的计算密集型任务,例如大规模数组的排序、图形的渲染、复杂算法的求解等。

应用场景

1、并行计算:

ForkJoinPool 提供了一种方便的方式来执行大规模的计算任务,并充分利用多核处理器的性能优势。通过将大任务分解成小任务,并通过工作窃取算法实现任务的并行执行,可以提高计算效率。

2、递归任务处理:

ForkJoinPool 特别适用于递归式的任务分解和执行。它可以将一个大任务递归地分解成许多小任务,并通过工作窃取算法动态地将这些小任务分配给工作线程执行。

3、并行流操作:

Java 8 引入了 Stream API,用于对集合进行函数式编程风格的操作。ForkJoinPool 通常用于执行并行流操作中的并行计算部分,例如对流中的元素进行过滤、映射、聚合等操作。

4、高性能任务执行:

ForkJoinPool 提供了一种高性能的任务执行机制,通过对任务进行动态调度和线程池管理,可以有效地利用系统资源,并在多核处理器上实现任务的并行执行。

总的来说,ForkJoinPool 类在 Java 中具有广泛的应用场景,特别适用于大规模的并行计算任务和递归式的任务处理。它通过工作窃取算法和任务分割合并机制,提供了一种高效的并行计算方式,可以显著提高计算效率和性能。

Fork/Join使用

Fork/Join框架的主要组成部分是ForkJoinPool、ForkJoinTask。ForkJoinPool是一个线程池,它用于管理ForkJoin任务的执行。ForkJoinTask是一个抽象类,用于表示可以被分割成更小部分的任务。

ForkJoinPool

ForkJoinPool是Fork/Join框架中的线程池类,它用于管理Fork/Join任务的线程。ForkJoinPool类包括一些重要的方法,例如submit()、invoke()、shutdown()、awaitTermination()等,用于提交任务、执行任务、关闭线程池和等待任务的执行结果。ForkJoinPool类中还包括一些参数,例如线程池的大小、工作线程的优先级、任务队列的容量等,可以根据具体的应用场景进行设置。

构造器

ForkJoinPool中有四个核心参数,用于控制线程池的并行数、工作线程的创建、异常处理和模式指定等。各参数解释如下:

  • int parallelism:指定并行级别(parallelism level)。ForkJoinPool将根据这个设定,决定工作线程的数量。如果未设置的话,将使用Runtime.getRuntime().availableProcessors()来设置并行级别。
  • ForkJoinWorkerThreadFactory factory:ForkJoinPool在创建线程时,会通过factory来创建。注意,这里需要实现的是ForkJoinWorkerThreadFactory,而不是ThreadFactory。如果你不指定factory,那么将由默认的DefaultForkJoinWorkerThreadFactory负责线程的创建工作。
  • UncaughtExceptionHandler handler:指定异常处理器,当任务在运行中出错时,将由设定的处理器处理。
  • boolean asyncMode:设置队列的工作模式。当asyncMode为true时,将使用先进先出队列,而为false时则使用后进先出的模式。
// 获取处理器数量
int processors = Runtime.getRuntime().availableProcessors();
// 构建forkjoin线程池
ForkJoinPool forkJoinPool = new ForkJoinPool(processors);

任务提交方式

任务提交是ForkJoinPool的核心能力之一,提交任务有三种方式:

返回值

方法

提交异步执行

void

execute(ForkJoinTask task)

execute(Runnable task)

等待并获取结果

T

invoke(ForkJoinTask task)

提交执行获取Future结果

ForkJoinTask

submit(ForkJoinTask task)

submit(Callable task)

submit(Runnable task)

submit(Runnable task, T result)

ForkJoinTask

ForkJoinTask是Fork/Join框架中的抽象类,它定义了执行任务的基本接口。用户可以通过继承ForkJoinTask类来实现自己的任务类,并重写其中的compute()方法来定义任务的执行逻辑。通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下三个子类:

  • RecursiveAction:用于递归执行但不需要返回结果的任务。
  • RecursiveTask :用于递归执行需要返回结果的任务。
  • CountedCompleter :在任务完成执行后会触发执行一个自定义的钩子函数

调用方法

ForkJoinTask 最核心的是 fork() 方法和 join() 方法,承载着主要的任务协调作用,一个用于任务提交,一个用于结果获取。

  • fork()——提交任务

fork()方法用于向当前任务所运行的线程池中提交任务。如果当前线程是ForkJoinWorkerThread类型,将会放入该线程的工作队列,否则放入common线程池的工作队列中。

  • join()——获取任务执行结果

join()方法用于获取任务的执行结果。调用join()时,将阻塞当前线程直到对应的子任务完成运行并返回结果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/940020.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python08-序列02-字典dict、集合set

一、字典&#xff08;dict&#xff09;&#xff1a;可变数据类型 1-1、字典的特点 字典是可变数据类型&#xff08;list也是&#xff09;&#xff0c;具有增、删、改等一系列的操作&#xff1b;字典中的元素是无序的&#xff08;hash&#xff09;key必须唯一&#xff0c;value…

【Java项目】基于SpringBoot的【旅游管理系统 】

【Java项目】基于SpringBoot的【旅游管理系统 】 技术简介&#xff1a;本系统使用JAVA语言开发&#xff0c;采用B/S架构、Spring Boot框架、MYSQL数据库进行开发设计。 系统简介&#xff1a;&#xff08;1&#xff09;管理员功能&#xff1a;可以管理个人中心、用户管理、景区分…

UE5 跟踪能力的简单小怪

A、思路 1、用素材的骨骼网格体创建小怪BP&#xff0c;绑定新的小怪控制器。 2、控制器的事件开始时&#xff0c;获取玩家状态&#xff0c;指定AI小怪自动向玩家移动。 复杂的AI需要用强大功能如黑板、行为树。 而简单的AI则可以用简单方法实现&#xff0c;杀鸡不用牛刀。视…

渗透测试学习笔记(五)网络

一.IP地址 1. IP地址详解 ip地址是唯一标识&#xff0c;一段网络编码局域网&#xff08;内网&#xff09;&#xff1a;交换机-网线-pcx.x.x.x 32位置2进制&#xff08;0-255&#xff09; IP地址五大类 IP类型IP范围A类0.0.0.0 到 127.255.255.255B类128.0.0.0 到191.255.25…

Windows 下 Anaconda的安装与配置 GPU 版

给之前的电脑安一下深度学习环境 判断是否有NVIDIA GPU Ctrl Shift Esc 打开任务管理器 带此字眼表示有 NVIDIA GPU 安装Anaconda anaconda 打开邮箱会看到下载链接 这里建议修改为其他盘,要不然下载的包和创建的环境都在C盘&#xff0c;占用空间 三个都打钩 取…

flutter --no-color pub get 超时解决方法

新建Flutter项目后&#xff0c;运行报错&#xff0c;需要执行pub get 点击Run ‘flutter pub get’ … … … 卡着&#xff0c;不动了&#xff0c;提示超时 是因为墙的问题 解决方案&#xff1a; 添加以下环境变量 变量名: PUB_HOSTED_URL 变量值: https://pub.flutter-io.cn …

Marin说PCB之POC电路layout设计仿真案例---06

我们书接上回啊&#xff0c;对于上面的出现原因我这个美女同事安娜说会不会你把POC电感下面的相邻两层的CUT_OUT的尺寸再去加大一些会不会变得更好呢&#xff1f;这个难道说是真的有用吗&#xff1f;小编我先自己算一卦看下结果。 本期文章我们就接着验证通过改善我们的单板POC…

Node.js 构建简单应用

在 Node.js 中构建一个简单应用通常包括以下几个步骤&#xff1a; 安装 Node.js设置项目目录初始化项目创建服务器并处理请求和响应 接下来&#xff0c;我们将一步步介绍如何用 Node.js 构建一个简单的 HTTP 应用程序。 1、安装 Node.js 首先确保系统上已安装 Node.js 和 n…

Cesium 无人机航线规划(航点航线)

航线规划实现定制航线&#xff0c;一键巡检功能 小镜头模拟的是此方向的拍照效果&#xff0c;觉得合适可以打个拍照印记 设置里可调控参数 保存后反显的样子&#xff0c;主要是为了区分航线

rfid标签打印开发指导

使用java连接斑马打印机&#xff0c;开发rfid标签打印功能 1.引用斑马打印机的SDKjar包 ZSDK_API.jar 将这个jar文件放到项目的lib目录下&#xff0c;没有就新建一个。 然后点击 File–Project Sreucture–Modules 点击加号 选择对应jar包即可 2.代码开发 1.打印机连接地址…

vue-office:Star 4.2k,款支持多种Office文件预览的Vue组件库,一站式Office文件预览方案,真心不错

嗨&#xff0c;大家好&#xff0c;我是小华同学&#xff0c;关注我们获得“最新、最全、最优质”开源项目和高效工作学习方法 vue-office 是一个支持多种文件格式&#xff08;docx、excel、pdf、pptx&#xff09;预览的Vue组件库&#xff0c;它不仅支持Vue2和Vue3&#xff0c;还…

Docker介绍、安装、namespace、cgroup、镜像-Dya 01

0. 容器简介 从生活上来说&#xff0c;容器是一种工具&#xff0c;可以装东西的工具&#xff0c;如衣柜、背包、行李箱等等。 从IT技术方面来说&#xff0c;容器是一种全新的虚拟化技术&#xff0c;它提高了硬件资源利用率&#xff0c;结合k8s还可以让企业业务快速横向扩容、业…

Kube-state-metrics 可观测性最佳实践

Kube-state-metrics 介绍 Kube-state-metrics 是 Kubernetes 生态系统中的一个开源项目&#xff0c;主要用来收集和报告集群中各种资源的实时状态信息。 工作原理 Kube-state-metrics 连接到 Kubernetes API 服务器&#xff0c;并公开一个 HTTP 端点&#xff0c;提供集群中各…

Pycharm配置Python开发环境

Pycharm配置Python开发环境 在之前的文章中,安装好了Pyhton和Pycharm。 打开Pycharm,如下图 配置完成之后,如下图所示:

scala中模式匹配的应用

package test34object test6 {case class Person(name:String)case class Student(name:String, className:String)// match case 能根据 类名和属性的信息&#xff0c;匹配到对应的类// 注意&#xff1a;// 1 匹配的时候&#xff0c;case class的属性个数要对上// 2 属性名不需…

PyQt5学习笔记

P95 绝对布局 绝对布局&#xff0c;使用move方法&#xff0c;操作坐标来控件控件的位置。 import sys from PyQt5.QtWidgets import *绝对布局&#xff0c;使用move方法&#xff0c;操作坐标来控件控件的位置。class MyWin(QWidget):def __init__(self):super().__init__()#…

Python3.13安装和配置

Python3.13安装和配置 一、Python的下载 点击下面的下载链接,下载需要的版本。以3.13版本为例。如下图所示: 3.13.0下载地址(windows)3.13.0下载地址(windows) 二、安装 下载完成后,双击安装文件。 <

探索Linux中的Zombie僵死进程

文章目录 探索Linux中的Zombie僵死进程什么是Zombie僵死进程&#xff1f;僵死进程的产生原因如何识别僵死进程&#xff1f;如何清理僵死进程&#xff1f;僵死进程对系统的影响总结 探索Linux中的Zombie僵死进程 在Linux系统中&#xff0c;进程管理是一个非常重要的主题&#x…

win11 C盘出现感叹号解决方法

出现感叹号&#xff0c;原因是对C盘进行了BitLocker驱动器加密操作。如果想去除感叹号&#xff0c;对C盘进行BitLocker解密即可。 步骤如下&#xff1a; 1.点击Windows搜索框 2.搜索框内输入 系统 3.按下回车&#xff0c;进入系统界面 4.点击隐私和安全性 点击BitLocker驱…

多个Echart遍历生成 / 词图云

echart官网 安装 如果版本报错推荐安装以下版本 npm install echarts4.8.0 --savenpm uninstall echarts//这个是卸载命令以下安装成功后是局部引入:多个Echart遍历生成 vue3echart单个页面多个图表循环渲染展示:<template><div class"main"><div …