Bootstrap

Java并发编程之Fork/Join框架和CompletableFuture的使用剖析!| 多线程篇(十)

环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8

一、前言

  还是原来的配方,还是儿时的味道,老样子!在继续深入学习Java并发编程之前,先来回顾一下上期《Java并发设计模式:生产者-消费者模式、读写锁模式与线程池模式!| 多线程篇(九)》内容,在通过上期教学中,我们主要深入学习了Java并发的三种设计模式,具体如下,以下是对上期内容的简要回顾:

  1. 生产者-消费者模式:它是一种经典的并发设计模式,通过协调生产者线程(生成数据)和消费者线程(消费数据)的工作,来实现线程间的数据交换。这种模式通常涉及到一个共享的缓冲区,生产者将数据放入缓冲区,而消费者从缓冲区取出数据进行处理。

  2. 读写锁模式:这个模式允许多个线程同时读取同一个资源,但写入操作是互斥的,以保证数据的一致性。通过使用读写锁,我们可以在不牺牲太多性能的情况下,实现对共享资源的安全访问。

  3. 线程池模式:它是一种资源管理策略,它通过重用一组预先创建的线程来执行任务,从而避免了频繁创建和销毁线程的开销。线程池模式可以显著提高程序的响应速度和资源利用率。

  那既然如此,只要通过上期内容的完整学习,我们就可以掌握Java并发中设计模式的常规使用,还能提升解决并发问题的能力。这些模式是Java并发编程中的重要组成部分之一,它们以一种高效且易于管理的方式来保证多线程环境下数据的一致性和完整性,对于你掌握多线程打下坚实的基础,并且在于你学设计模式也有很好的理解作用。

  至此,对于我们学习多线程编程篇章就快要告一段落了,对于大家是否能真正掌握它就得靠大家的自觉与勤奋了。现在,让我们带着前九期讲的这些知识点,继续深入探讨Java并发编程模块中的一个重要知识点--高级主题,例如Fork/Join框架、CompletableFuture等。学习这些高级主题将帮助我们进一步提升程序并发编程能力,编写出更加高效、稳定和可维护的多线程项目,也为我们在日后的开发中,实现程序线程安全铺路,最终辅助大家都能在项目实践中,将所学知识点彻底发挥作用。

二、摘要

  对于目前项目开发而言,多线程和并发是提升应用性能的关键技术之一。但我们也都清楚,Java作为一门广泛使用的编程语言,本身就提供了丰富的并发工具和框架。顾,本文我将带着大家深入探讨Java中的并发编程,包括Fork/Join框架和CompletableFuture,以及并发编程的最佳实践,将其知识点统统分享给大家。对于分享,整理全文,我会详细介绍Java中的高级并发编程技术,包括Fork/Join框架、CompletableFuture以及并发的最佳实践等;通过源码解析、案例分析和应用场景的介绍,通过理论与实践相结合的方式,以最优的阅读模式帮助大家迅速掌握。基于本文,我旨在帮助你们理解并应用这些技术,以提高程序的并发性能和稳定性,写出一手优雅并高逼格的代码,这是我们每一个开发者心中的追求。

三、正文

3.1 简介

  在Java中,多线程编程是实现并发的主要手段。随着多核处理器的普及,编写高效的并发程序变得尤为重要。Java提供了多种并发工具,其中Fork/Join框架和CompletableFuture是两个重要的工具,如下我便要重点来进行介绍及深究它两,你们有咩有做好准备??

3.2 Fork/Join 框架

3.2.1 简介

  首先,我们先来聊聊它--Fork/Join框架。提到框架,我们肯定或多或少都接触过不少,但是对于这种框架,你们是否有了解过?在今天你不清楚也没关系,那你听我讲好了。说起Fork/Join框架,它是Java 7中引入的一种用于并行处理任务的框架,特别适合于可以递归分解为更小任务的大规模问题;它的核心思想是将一个大任务分解为多个小任务,然后并行地执行这些小任务,并最终将结果合并。

3.2.2 工作原理

  其次,我们既然已经知道它为何物,接下来我们就应该深究一下它工作原理,对于Fork/Join框架,工作原理主要可以概括为以下几个流程:

  1. 任务分解:将一个大任务分解成若干个更小的子任务。
  2. 任务执行:将这些子任务分配给不同的线程并行执行。
  3. 结果合并:当所有子任务执行完毕后,将它们的结果合并起来。

  表面上看上去,并无多复杂,实则也确实并不复杂。

这边整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

 需要全套面试笔记的【点击此处即可】免费获取

3.2.3 核心组件

  然后,对于Fork/Join框架,它主要由以下几个核心组件构成:

  • ForkJoinPool:一个特殊的线程池,用于执行ForkJoinTask。
  • ForkJoinTask:一个抽象类,定义了任务的执行和结果合并的方法。
  • RecursiveAction:ForkJoinTask的子类,用于没有返回结果的任务。
  • RecursiveTask:ForkJoinTask的子类,用于有返回结果的任务。

3.2.4 使用场景

  可能学完这些相关知识点,大家好奇心最重的可能会问学了它能干啥,这也是我最关心的问题之一。其实,对于Fork/Join框架,它适用于那些可以被分解为多个子任务,并且子任务之间相对独立的场景,比如常见的使用场景:

  • 大数据集的并行处理:例如,对大数据集进行排序或搜索。
  • 递归算法的并行化:例如,树或图的遍历算法。
  • 分块处理:例如,对大型文件进行分块处理。

  除了以上我举例的,还有很多很多适用的场景,这都需要大家去挖掘去探索,通过举一反三的思维,真正将该知识点运用到日常中去,而不是让其成为无人问津的高阶玩物。

3.2.5 示例代码

  说了这么多,接着我便通过一个实例来给大家亲自感受一下啊,通过案例演示使用Fork/Join框架来实现一个并行的斐波那契数列计算器。

3.2.5.1 示例代码

示例代码如下:

 

java

代码解读

复制代码

package com.secf.service.port.day10; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.RecursiveTask; /** * Fork/Join框架实现并行的斐波那契数列计算器 * * @Author bug菌 * @Source 公众号:猿圈奇妙屋 * @Date 2024年7月2日10:11:33 */ class Fibonacci extends RecursiveTask<Integer> { private final int n; public Fibonacci(int n) { this.n = n; } @Override protected Integer compute() { if (n <= 1) { return n; } Fibonacci f1 = new Fibonacci(n - 1); Fibonacci f2 = new Fibonacci(n - 2); f1.fork(); // 异步执行 return f2.compute() + f1.join(); // 等待f1执行完成并获取结果 } public static void main(String[] args) { Fibonacci task = new Fibonacci(10); ForkJoinPool pool = new ForkJoinPool(); int result = pool.invoke(task); System.out.println("Fibonacci(10) = " + result); } }

3.2.5.2 示例代码执行结果

  根据如上的测试用例,作者在本地进行测试结果如下,仅供参考,你们也可以自行修改测试用例或者添加其他的测试数据或测试方法,以便于进行熟练学习以此加深知识点的理解。

3.2.5.3 示例代码分析

  在本次的代码演示中,我将会深入剖析每句代码,详细阐述其背后的设计思想和实现逻辑。通过这样的讲解方式,我希望能够引导同学们逐步构建起对代码的深刻理解。我会先从代码的结构开始,逐步拆解每个模块的功能和作用,并指出关键的代码段,并解释它们是如何协同运行的。通过这样的讲解和实践相结合的方式,我相信每位同学都能够对代码有更深入的理解,并能够早日将其掌握,应用到自己的学习和工作中。

  如上示例代码我主要是实现了一个基于Fork/Join框架的并行斐波那契数列计算器。以下是对其代码的详细分析,仅供参考:

1. Fibonacci类的定义

  Fibonacci类继承自RecursiveTask<Integer>,是一个用于计算斐波那契数列的任务。RecursiveTask是Fork/Join框架中的一个抽象类,用于返回结果的任务。Fibonacci类有一个私有的整数成员变量n,表示需要计算的斐波那契数列的第n项。

2. 构造函数

  构造函数接受一个整数参数n,并将其赋值给成员变量n,表示需要计算的目标项。

3. compute()方法

  compute()方法是Fork/Join任务的核心逻辑,定义了任务如何执行:

  • 基本情况: 如果n小于或等于1,则直接返回n,因为斐波那契数列的第0项为0,第1项为1。
  • 递归计算: 如果n大于1,代码会创建两个新的Fibonacci任务,分别计算n-1n-2项的斐波那契数。
    • f1.fork():异步执行f1任务(计算n-1项),这意味着它会在独立的线程中执行,不会阻塞当前线程。
    • return f2.compute() + f1.join();:在当前线程中执行f2任务(计算n-2项),然后使用f1.join()等待f1任务的完成,并将两者的结果相加返回。

4. main()方法 main()方法是程序的入口:

  • 创建任务: 使用10作为参数创建一个Fibonacci任务实例,这意味着将计算斐波那契数列的第10项。
  • 创建ForkJoinPool: 创建一个ForkJoinPool实例,这是Java中用于并行执行任务的线程池。
  • 执行任务: 使用pool.invoke(task)调用任务并等待其完成,结果会被存储在result中。
  • 输出结果: 最后,将计算得到的第10项斐波那契数列的值打印出来。

3.2.6 优缺点分析

  针对Fork/Join 框架,有利有弊,如下是我汇总的一些比较常关注的点,希望大家在技术选型时,能够考虑清楚,以下是对Fork/Join框架的优缺点分析:

优点总结如下:

  1. 提高性能:通过将大任务分解成小任务并行处理,Fork/Join框架可以显著提高程序的执行效率,特别是在多核处理器上。
  2. 简化并行编程:框架提供了一种相对简单的方法来实现并行计算,开发者只需关注任务的分解和结果的合并,而不需要管理线程的创建和调度。
  3. 自动负载均衡:ForkJoinPool线程池能够自动进行工作窃取(work-stealing),在某些线程空闲时,它们可以"偷取"其他线程的任务来执行,实现负载均衡。
  4. 适用性广泛:适用于许多可以分解为多个子任务的问题,如大规模数据处理、递归算法、图像处理等。
  5. 可扩展性:可以根据需要调整线程池的大小,以适应不同的计算资源和任务特性。

缺点总结如下:

  1. 任务分解的复杂性:不是所有的问题都容易分解为小任务,设计合适的任务分解策略可能需要深入思考和精心设计。
  2. 合并结果的开销:在某些情况下,合并多个子任务结果的开销可能会影响性能,特别是当任务分解得过细时。
  3. 可能的资源浪费:如果任务分解不当或任务执行时间差异较大,可能会导致某些线程空闲,而其他线程过载,从而造成资源浪费。
  4. 调试难度:并行程序的调试通常比单线程程序更加困难,因为它们涉及多个线程的交互和潜在的并发问题。
  5. 不适合I/O密集型任务:Fork/Join框架主要针对CPU密集型任务设计,对于I/O密集型任务,可能不是最合适的选择,因为I/O操作的等待时间可能会降低并行计算的优势。 

3.2.7 小结

  基于如上使用,可以感受到Fork/Join框架它是一个强大的工具,适用于处理可以并行分解的任务。然而,它也需要你们对任务分解和并行计算有深入的理解,以避免设计不当带来的性能问题。在使用Fork/Join框架时,应该权衡其优缺点,根据具体的应用场景和需求做出合理的技术选型,这才是重中之重。

3.3 CompletableFuture

  讲解完了Fork/Join框架,接着我们来聊聊CompletableFuture,可能有些小伙伴就不是很了解了,它是 Java 8 中引入的一个非常强大的类,它提供了一个可异步计算的 Future 的扩展版本,允许开发者以声明的方式处理异步逻辑。以下是对CompletableFuture 的详细解读,如果你是新手,还请务必好好学。

3.3.1 简介

  对于CompletableFuture,可能有些小伙伴就不是很了解了,它是 Java 8 中引入的一个非常强大的类,它提供了一个可异步计算的 Future 的扩展版本,允许开发者以声明的方式处理异步逻辑。以下是对CompletableFuture 的详细解读,如果你是新手,还请务必好好学。

  CompletableFuture它支持多种回调,譬如 thenApplythenAcceptthenRun 等,这些回调可以在异步计算完成时触发。此外,它还支持异常处理和组合多个异步操作。

3.3.2 核心特性

  对于CompletableFuture,它跟Fork/Join框架一样具有特性,如下我简单介绍一下它的几个核心特性,例如:

  • 非阻塞:可以在其他线程中异步执行任务,而不会阻塞当前线程。
  • 链式调用:可以链接多个异步操作,每个操作都在前一个操作完成后执行。
  • 异常处理:可以添加异常处理逻辑,以便在异步操作中出现异常时进行处理。
  • 组合操作:可以组合多个 CompletableFuture 对象,实现更复杂的异步逻辑。

3.3.3 示例代码

  我主张理论与实际相结合,以下是我写的一个简单示例,目的是为了演示如何常规使用 CompletableFuture 来实现异步操作,仅供参考:

3.3.3.1 代码演示

示例代码如下,大家可以跟着手敲一遍,熟悉熟悉。

 

java

代码解读

复制代码

/** * CompletableFuture示例演示 * * @Author bug菌 * @Source 公众号:猿圈奇妙屋 * @Date 2024年7月2日10:11:33 */ public class CompletableFutureTest { public static void main(String[] args) { CompletableFuture.supplyAsync(() -> { // 模拟一个耗时的计算任务 try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return 42; }).thenApply(result -> { // 对结果进行处理 return result * 2; }).thenAccept(result -> { // 接受处理结果 System.out.println("Result: " + result); }).exceptionally(ex -> { // 异常处理 System.out.println("Error occurred: " + ex.getMessage()); return null; }); } }

3.3.3.2 示例代码执行结果

  根据如上的测试用例,作者在本地进行测试结果如下,仅供参考,你们也可以自行修改测试用例或者添加其他的测试数据或测试方法,以便于进行熟练学习以此加深知识点的理解。

3.3.3.3 代码解析

  接着我将对如上案例代码进行讲解,我主要是为了展示如何使用CompletableFuture来处理异步任务。CompletableFuture是Java 8引入的一个类,用于简化异步编程,它允许你编写非阻塞的异步代码,同时提供丰富的API来处理任务的结果或异常。

以下是这段代码的详细解析:

1. 类和main()方法定义

  CompletableFutureTest类包含一个main()方法,这是Java程序的入口。所有的代码逻辑都在main()方法中实现。

2. 异步任务的创建

  • CompletableFuture.supplyAsync:该方法接收一个Supplier函数接口的实现,并在一个独立的线程中异步执行该任务。Supplier接口是一个无参数、有返回值的函数接口。
  • 耗时任务的模拟: Thread.sleep(1000)模拟了一个耗时1秒的计算任务。任务完成后,返回值42作为结果。

3. 处理任务结果

  • thenApply:这是一个链式方法,它在前一个任务完成并返回结果后执行。thenApply接收一个函数,该函数将对前一个任务的结果进行处理。在这个例子中,传入的函数将结果乘以2并返回。

4. 接受最终结果

  • thenAccept:这个方法在前一个任务完成后执行,但它不会返回新的结果,而是消费前一个任务的结果。这里的操作是打印出最终计算的结果Result: 84

5. 异常处理

  • exceptionally:该方法用于处理异步计算过程中发生的异常。如果在任何阶段发生异常,exceptionally会被调用,它接收异常作为参数,并允许你处理异常并返回一个替代值。在此例中,如果发生异常,它会打印出错误信息,并返回null

6. 异步调用的执行

  CompletableFuture的异步调用是非阻塞的,这意味着main()方法会立即返回而不会等待异步任务完成。如果你运行这段代码,你可能不会看到输出,因为主线程可能已经结束了,导致JVM提前终止。为了保证看到结果输出,通常需要在主线程中添加一些等待逻辑,或者在调试环境中运行。

3.3.4 应用场景

  CompletableFuture 适用于需要执行异步操作并且需要对结果进行进一步处理的场景。例如:

  • Web服务:异步处理用户请求并返回响应。
  • 数据处理:异步加载和处理数据。
  • 资源密集型任务:避免阻塞主线程,提高应用响应性。

3.3.5 优缺点分析

  这里,我们系统来汇总分析一下,基于如上所讲解的知识点中,学完之后我们可以对其有个很直观地辩证使用,如下是我总结的优缺点分析,希望学完后能够帮助到大家学习并正常使用它!

  • 优点
  1. 异步编程简化CompletableFuture 提供了一种现代的、更简洁的方式来编写异步代码,避免了传统回调地狱(callback hell)的问题。
  2. 链式调用:支持方法链式调用,使得异步操作的编排更加直观和灵活。
  3. 功能丰富:提供了thenApplythenAcceptthenRunexceptionally等多种方法,可以方便地对异步结果进行处理。
  4. 错误处理:内置了异常处理机制,使得异步操作中的错误管理更加简单。
  5. 组合操作:可以轻松地组合多个异步操作,例如使用CompletableFuture.allOfCompletableFuture.anyOf来等待多个异步任务的完成。
  6. 支持默认值:在异步操作尚未完成时,可以提供默认值,使用orElseorElseGet等方法。
  7. 支持取消操作:如果不再需要异步操作的结果,可以取消正在进行的异步任务,释放资源。
  • 缺点
  1. 学习曲线:对于不熟悉异步编程的开发者,CompletableFuture 的概念和API可能需要一定的时间来学习和掌握。
  2. 调试难度:异步代码的调试通常比同步代码更加困难,因为它们涉及线程间的交互和时间上的不确定性。
  3. 过度使用可能导致复杂性:如果过度使用CompletableFuture的链式调用和组合,可能会导致代码难以理解和维护。
  4. 资源消耗:虽然CompletableFuture可以异步执行任务,但如果不正确管理,可能会造成线程资源的浪费。
  5. 执行顺序问题:在某些情况下,CompletableFuture的执行顺序可能不如同步代码直观,需要仔细设计以确保正确的执行顺序。
  6. 对JDK版本的依赖CompletableFuture 是Java 8引入的,这意味着它不能在旧版本的Java平台上使用,这可能限制了其在某些项目中的使用。

3.3.6 小结

  CompletableFuture 作为是Java并发编程中一个强大的工具,它提供了一种现代、灵活的方式来处理异步编程问题。然而,它也需要开发者具备一定的异步编程知识,以避免由于不当使用导致的复杂性和性能问题。在使用CompletableFuture时,应该根据实际需求和项目环境,权衡其优缺点,做出合适的选择,就跟是否使用Fork/Join 框架一样。

3.4 小结

  在本次章节讲解中,我讲解了Fork/Join框架与CompletableFuture,结合理论与实际进行双讲解,以此辅助大家增强对其的理解与使用。这些框架及工具类不仅可以提升了我们对Java并发机制的理解,也能提升我们构建高效并发应用的能力。

3.4.1 Fork/Join框架

  Fork/Join框架,它以其独特的方式简化了并行任务的处理。它通过将大任务分解成小任务,利用了现代多核处理器的计算能力,实现了任务的高效并行处理。然而,这种框架需要开发者精心设计任务的分解和合并策略,以避免因不当的分解导致的性能问题。

3.4.2 CompletableFuture

  CompletableFuture,它代表了Java异步编程的一大步进。它不仅提供了非阻塞的编程模式,还允许我们以声明式的方式链接多个异步操作,极大地简化了异步逻辑的复杂性。尽管如此,合理地使用CompletableFuture需要对异常处理和链式调用有深刻的理解,以防止代码的复杂度过高。

;