87、Java Fork/Join框架:并行编程的利器

Java Fork/Join框架:并行编程的利器

在当今的软件开发中,充分利用多核处理器的并行计算能力是提高程序性能的关键。Java的Fork/Join框架为此提供了一种简单而有效的方式,它允许开发者以清晰、可扩展的方式利用多个处理器。

1. 传统多线程与并行编程的区别

在过去,大多数计算机只有单个CPU,多线程主要用于利用空闲时间,例如程序等待用户输入时。在单CPU系统中,多线程允许两个或多个任务共享CPU,这种多线程通常由 Thread 对象支持。然而,这种多线程方式在多核计算机中并非最优选择。

当系统中有多个CPU时,就需要支持真正并行执行的多线程能力。多个CPU可以同时执行程序的不同部分,每个部分在自己的CPU上运行,这可以显著加快某些操作的执行速度,如对大型数组进行排序、转换或搜索。

2. Fork/Join框架概述

Fork/Join框架位于 java.util.concurrent 包中,它通过两种重要方式增强了多线程编程:
- 简化了多线程的创建和使用。
- 自动利用多个处理器,使应用程序能够自动扩展以利用可用处理器的数量。

这些特性使得Fork/Join框架成为并行处理时推荐的多线程方法。

3. 主要的Fork/Join类

Fork/Join框架的核心包含四个主要类:
| 类名 | 描述 |
| ---- | ---- |
| ForkJoinTask<V> | 定义任务的抽象类 |
| ForkJoinPool | 管理 ForkJoinTask 的执行 |
| RecursiveAction | ForkJoinTask<V> 的子类,用于不返回值的任务 |
| RecursiveTask<V> | ForkJoinTask<V> 的子类,用于返回值的任务 |

它们之间的关系如下: ForkJoinPool 管理 ForkJoinTask 的执行, ForkJoinTask 是一个抽象类, RecursiveAction RecursiveTask 是它的抽象子类,通常我们会扩展这两个子类来创建任务。

4. ForkJoinTask类

ForkJoinTask<V> 是一个抽象类,定义了可以由 ForkJoinPool 管理的任务。与 Thread 不同, ForkJoinTask 是任务的轻量级抽象,由 ForkJoinPool 管理的线程执行。

ForkJoinTask 定义了许多方法,核心方法有 fork() join()
- final ForkJoinTask<V> fork() :提交调用任务进行异步执行,调用 fork() 的线程会继续运行,任务调度执行后返回 this 。在JDK 8之前, fork() 只能在另一个在 ForkJoinPool 中运行的 ForkJoinTask 的计算部分内执行;JDK 8引入了公共池后,如果 fork() 不在 ForkJoinPool 内调用,会自动使用公共池。
- final V join() :等待调用该方法的任务终止,并返回任务的结果。

另外, invoke() 方法将 fork join 操作合并为一个调用,开始一个任务并等待其结束,返回调用任务的结果。 invokeAll() 方法可以同时执行多个任务,有两种形式:
- static void invokeAll(ForkJoinTask<?> taskA, ForkJoinTask<?> taskB)
- static void invokeAll(ForkJoinTask<?> ... taskList)

5. RecursiveAction类

RecursiveAction ForkJoinTask 的子类,封装了不返回结果的任务。通常,我们会扩展 RecursiveAction 来创建返回类型为 void 的任务。 RecursiveAction 指定了四个方法,但通常只有抽象方法 compute() 是我们关注的:

protected abstract void compute()

当扩展 RecursiveAction 创建具体类时,需要将定义任务的代码放在 compute() 方法中,该方法代表任务的计算部分。

6. RecursiveTask 类

RecursiveTask<V> 也是 ForkJoinTask 的子类,封装了返回结果的任务,结果类型由 V 指定。通常,我们会扩展 RecursiveTask<V> 来创建返回值的任务。和 RecursiveAction 一样,它也指定了四个方法,常用的也是抽象方法 compute()

protected abstract V compute()

实现 compute() 方法时,必须返回任务的结果。

7. ForkJoinPool类

ForkJoinTask 的执行在 ForkJoinPool 中进行, ForkJoinPool 也管理任务的执行。从JDK 8开始,有两种方式获取 ForkJoinPool
- 手动使用 ForkJoinPool 构造函数创建。
- 使用公共池,公共池是JDK 8添加的静态 ForkJoinPool ,可自动使用。

ForkJoinPool 定义了几个构造函数,常用的有:
- ForkJoinPool() :创建一个默认池,支持的并行级别等于系统中可用处理器的数量。
- ForkJoinPool(int pLevel) :允许指定并行级别,该值必须大于零且不超过实现的限制。

创建 ForkJoinPool 实例后,可以通过多种方式启动任务:
- <T> T invoke(ForkJoinTask<T> task) :开始指定的任务,并返回任务的结果,调用代码会等待 invoke() 返回。
- void execute(ForkJoinTask<?> task) :启动任务,但调用代码不会等待任务完成,而是继续异步执行。

使用公共池启动任务有两种基本方式:
- 调用 commonPool() 方法获取公共池的引用,然后使用该引用调用 invoke() execute()
- 在任务的计算部分之外调用 ForkJoinTask fork() invoke() 方法,会自动使用公共池。

ForkJoinPool 使用工作窃取算法管理线程的执行,每个工作线程维护一个任务队列,如果一个工作线程的队列为空,它会从另一个工作线程那里获取任务,这有助于提高整体效率和保持负载平衡。此外, ForkJoinPool 使用守护线程,当所有用户线程终止时,守护线程会自动终止,因此通常不需要显式关闭 ForkJoinPool ,但除公共池外,可以通过调用 shutdown() 方法关闭。

8. 分治策略

Fork/Join框架的用户通常会采用基于递归的分治策略,这也是 RecursiveAction RecursiveTask 这两个子类名称的由来。分治策略的基本思想是将一个任务递归地分解为更小的子任务,直到子任务的大小足够小,可以顺序处理。

例如,对一个包含N个整数的数组中的每个元素应用转换操作,可以将其分解为两个子任务,每个子任务处理数组的一半元素。这个过程会一直持续,直到达到一个阈值,此时顺序处理比进一步分解更高效。

选择合适的阈值是有效使用分治策略的关键,通常可以通过性能分析和实验来确定最优阈值。一般来说,一个任务应该执行100到10000个计算步骤。

9. 简单的Fork/Join示例

下面是一个简单的示例,展示了如何使用Fork/Join框架和分治策略将一个双精度数组中的元素转换为它们的平方根:

// A simple example of the basic divide-and-conquer strategy.
// In this case, RecursiveAction is used.
import java.util.concurrent.*;
import java.util.*;

// A ForkJoinTask (via RecursiveAction) that transforms
// the elements in an array of doubles into their square roots.
class SqrtTransform extends RecursiveAction {
  // The threshold value is arbitrarily set at 1,000 in this example.
  // In real-world code, its optimal value can be determined by
  // profiling and experimentation.
  final int seqThreshold = 1000;

  // Array to be accessed.
  double[] data;

  // Determines what part of data to process.
  int start, end;

  SqrtTransform(double[] vals, int s, int e ) {
    data = vals;
    start = s;
    end = e;
  }

  // This is the method in which parallel computation will occur.
  protected void compute() {

    // If number of elements is below the sequential threshold,
    // then process sequentially.
    if((end - start) < seqThreshold) {
      // Transform each element into its square root.
      for(int i = start; i < end; i++) {
         data[i] = Math.sqrt(data[i]);
      }
    }
    else {
      // Otherwise, continue to break the data into smaller pieces.

      // Find the midpoint.
      int middle = (start + end) / 2;

      // Invoke new tasks, using the subdivided data.
      invokeAll(new SqrtTransform(data, start, middle),
                new SqrtTransform(data, middle, end));
    }
  }
}

// Demonstrate parallel execution.
class ForkJoinDemo {
  public static void main(String args[]) {
    // Create a task pool.
    ForkJoinPool fjp = new ForkJoinPool();

    double[] nums = new double[100000];

    // Give nums some values.
    for(int i = 0; i < nums.length; i++)
      nums[i] = (double) i;

    System.out.println("A portion of the original sequence:");

    for(int i=0; i < 10; i++)
      System.out.print(nums[i] + " ");
    System.out.println("\n");

    SqrtTransform task = new SqrtTransform(nums, 0, nums.length);

    // Start the main ForkJoinTask.
    fjp.invoke(task);

    System.out.println("A portion of the transformed sequence" +
                       " (to four decimal places):");
    for(int i=0; i < 10; i++)
      System.out.format("%.4f ", nums[i]);
    System.out.println();
  }
}

在这个示例中, SqrtTransform 类扩展了 RecursiveAction seqThreshold 变量指定了顺序处理的阈值, compute() 方法根据元素数量决定是顺序处理还是继续分解任务。

从JDK 8开始,我们可以使用公共池来简化代码,将 ForkJoinPool 的构造函数调用替换为 ForkJoinPool.commonPool()

ForkJoinPool fjp = ForkJoinPool.commonPool(); 

通过这个示例,我们可以看到Fork/Join框架和分治策略的强大之处,它们可以帮助我们充分利用多核处理器的性能,提高程序的执行效率。

Java Fork/Join框架:并行编程的利器

10. 示例代码详细分析

我们来更深入地分析上述示例代码的执行流程,通过以下流程图可以更清晰地看到程序的运行逻辑:

graph TD;
    A[Main方法开始] --> B[创建ForkJoinPool];
    B --> C[创建double数组nums并赋值];
    C --> D[创建SqrtTransform任务];
    D --> E[ForkJoinPool调用invoke方法启动任务];
    E --> F{SqrtTransform的compute方法};
    F -- 元素数量 < seqThreshold --> G[顺序处理元素];
    F -- 元素数量 >= seqThreshold --> H[计算中间点];
    H --> I[调用invokeAll启动两个子任务];
    I --> J{子任务的compute方法};
    J -- 元素数量 < seqThreshold --> K[顺序处理元素];
    J -- 元素数量 >= seqThreshold --> H;
    G --> L[所有任务完成,数组元素转换完成];
    L --> M[输出转换后的数组部分元素];

SqrtTransform 类中, compute() 方法是核心逻辑所在。当元素数量小于 seqThreshold 时,直接对元素进行平方根计算;当元素数量大于等于 seqThreshold 时,将任务分解为两个子任务,分别处理数组的前半部分和后半部分。这种递归分解的方式正是分治策略的体现。

11. 性能优化建议

在使用Fork/Join框架时,为了获得更好的性能,我们可以考虑以下几点:
- 合理设置阈值 :阈值的选择对性能影响很大。如果阈值设置过小,会导致任务分解过于频繁,增加线程创建和管理的开销;如果阈值设置过大,可能无法充分利用多核处理器的并行能力。可以通过性能分析工具(如VisualVM)对不同阈值进行测试,找到最优值。
- 避免共享资源竞争 :在多线程环境下,共享资源的访问可能会导致竞争和锁的开销。尽量减少对共享资源的访问,如果必须访问,使用线程安全的数据结构或同步机制。
- 利用公共池 :从JDK 8开始,公共池的引入简化了 ForkJoinPool 的使用。对于大多数应用程序,公共池可以提供足够的并行能力,并且可以自动管理线程资源。

12. 适用场景总结

Fork/Join框架适用于以下场景:
- 可分解的任务 :任务可以递归地分解为更小的子任务,并且子任务之间相互独立,例如对大型数组的排序、搜索、转换等操作。
- 多核处理器环境 :在多核处理器系统中,Fork/Join框架可以充分利用多个处理器的并行计算能力,提高程序的执行效率。
- 需要高性能的计算任务 :对于一些计算密集型的任务,如科学计算、图像处理等,使用Fork/Join框架可以显著提升性能。

13. 与其他并发工具的比较

Java提供了多种并发工具,如 Thread ExecutorService 等,与它们相比,Fork/Join框架有以下特点:
| 并发工具 | 特点 |
| ---- | ---- |
| Thread | 传统的多线程方式,需要手动管理线程的创建和销毁,适用于简单的多线程场景,但在处理大量任务时效率较低。 |
| ExecutorService | 提供了线程池的功能,简化了线程的管理,但对于可分解的任务,需要手动实现任务的分解逻辑。 |
| Fork/Join框架 | 专门为并行处理可分解的任务设计,自动实现任务的分解和合并,能够充分利用多核处理器的性能。 |

14. 总结

Java的Fork/Join框架为开发者提供了一种强大的并行编程工具,通过分治策略和工作窃取算法,能够高效地利用多核处理器的计算能力。在实际应用中,我们可以根据任务的特点和系统环境,合理使用Fork/Join框架,提高程序的性能和可扩展性。

通过本文的介绍,我们了解了Fork/Join框架的基本概念、主要类的使用、分治策略的实现以及性能优化建议。希望这些内容能够帮助你在开发中更好地应用Fork/Join框架,提升程序的性能。

在未来的软件开发中,随着多核处理器的普及,并行编程将变得越来越重要。Fork/Join框架作为Java中并行编程的重要工具,值得我们深入学习和掌握。

【永磁同步电机】基于模型预测控制MPC的永磁同步电机非线性终端滑模控制仿真研究(Simulink&Matlab代码实现)内容概要:本文围绕永磁同步电机(PMSM)的高性能控制展开,提出了一种结合模型预测控制(MPC)与非线性终端滑模控制(NTSMC)的先进控制策略,并通过Simulink与Matlab进行系统建模与仿真验证。该方法旨在克服传统控制中动态响应慢、鲁棒性不足等问题,利用MPC的多步预测和滚动优化能力,结合NTSMC的强鲁棒性和有限时间收敛特性,实现对电机转速和电流的高精度、快速响应控制。文中详细阐述了系统数学模型构建、控制器设计流程、参数整定方法及仿真结果分析,展示了该复合控制策略在抗干扰能力和动态性能方面的优越性。; 适合人群:具备自动控制理论、电机控制基础知识及一定Matlab/Simulink仿真能力的电气工程、自动化等相关专业的研究生、科研人员及从事电机驱动系统开发的工程师。; 使用场景及目标:①用于深入理解模型预测控制与滑模控制在电机系统中的融合应用;②为永磁同步电机高性能控制系统的仿真研究与实际设计提供可复现的技术方案与代码参考;③支撑科研论文复现、课题研究或工程项目前期验证。; 阅读建议:建议读者结合提供的Simulink模型与Matlab代码,逐步调试仿真环境,重点分析控制器设计逻辑与参数敏感性,同时可尝试在此基础上引入外部扰动或参数变化以进一步验证控制鲁棒性。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值