JAVA基础第一弹-ForkJoin解析

75 阅读6分钟

大家好,今天和大家一起学习一下ForkJoin~

Fork/Join 框架介绍

Fork/Join 框架是 Java 并发编程中的一个重要工具,它通过“分而治之”的策略来并行处理任务,特别适用于可以分解为多个子任务的计算密集型任务。下面将详细介绍 Fork/Join 框架的原理及其核心机制。

1. 基本概念

  • Fork:将一个大任务分解为多个子任务,每个子任务可以独立执行。
  • Join:等待所有子任务完成后,将子任务的结果合并,形成最终结果。

2. 核心组件

  • ForkJoinPool:这是 Fork/Join 框架的核心类,它是一个特殊的线程池,专门用于管理和调度 Fork/Join 任务。ForkJoinPool 内部维护了一个工作窃取队列(Work Stealing Queue),每个线程都有自己的双端队列(Deque),用于存储任务。
  • RecursiveTask 和 RecursiveAction
    • RecursiveTask:用于有返回值的任务。
    • RecursiveAction:用于没有返回值的任务。

3. 工作窃取(Work Stealing)

工作窃取是一种任务调度机制,旨在提高多核处理器的利用率。其基本思想是,当某个线程的任务队列为空时,该线程可以从其他线程的任务队列中“窃取”任务来执行。这样可以确保所有线程都能保持忙碌状态,避免某些线程空闲而其他线程过载的情况。

  • 任务队列:每个线程都有一个双端队列(Deque),用于存储任务。
  • 窃取机制:当一个线程的任务队列为空时,它会尝试从其他线程的任务队列的尾部窃取任务。这种机制确保了任务的均匀分布,提高了并行处理的效率。

4. 任务分解与合并

  • 任务分解:当一个任务太大时,可以将其分解为多个子任务。每个子任务可以进一步分解,直到达到一个可以直接解决的程度。
  • 任务合并:子任务完成后,需要将子任务的结果合并,形成最终结果。

5. 递归分解

Fork/Join 框架支持递归分解任务。通常,任务的分解过程是通过递归调用来实现的。当任务分解到一定程度时,可以使用 compute 方法直接计算结果,而不是继续分解。

6. 关键方法

  • fork() :将一个子任务提交到 ForkJoinPool 中,异步执行。
  • join() :等待子任务完成并返回结果。
  • compute() :计算任务的结果,可以递归调用 fork() 和 join()。

示例代码1:计算数组元素之和

下面是一个使用 Fork/Join 框架计算数组元素之和的例子。我们将使用 RecursiveTask 类来实现这个功能,因为我们需要返回计算结果。

import java.util.concurrent.RecursiveTask;

public class SumCalculator extends RecursiveTask<Integer> {
    private static final int THRESHOLD = 10; // 分解阈值
    private final int[] data;
    private final int start;
    private final int end;

    public SumCalculator(int[] data, int start, int end) {
        this.data = data;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int length = end - start;
        if (length <= THRESHOLD) {
            return sumRange(start, end);
        } else {
            int mid = (start + end) / 2;
            SumCalculator leftTask = new SumCalculator(data, start, mid);
            SumCalculator rightTask = new SumCalculator(data, mid, end);

            leftTask.fork(); // 异步执行子任务
            int rightResult = rightTask.compute(); // 同步执行子任务并等待结果
            int leftResult = leftTask.join(); // 等待异步子任务完成并获取结果
            return leftResult + rightResult;
        }
    }

    private int sumRange(int start, int end) {
        int sum = 0;
        for (int i = start; i < end; i++) {
            sum += data[i];
        }
        return sum;
    }

    public static void main(String[] args) {
        int[] numbers = new int[10000];
        for (int i = 0; i < numbers.length; i++) {
            numbers[i] = i; // 初始化数组
        }

        ForkJoinPool pool = new ForkJoinPool();
        SumCalculator task = new SumCalculator(numbers, 0, numbers.length);
        int result = pool.invoke(task);
        System.out.println("Sum: " + result);
    }
}

在这个例子中,我们创建了一个 SumCalculator 类,继承自 RecursiveTask,用于计算数组的一部分之和。当任务大小超过预设的阈值时,任务会被进一步分解为两个更小的任务,分别计算数组的前半部分和后半部分的和。最后,通过 ForkJoinPool 调用 invoke 方法启动任务,并收集所有子任务的结果以得到最终答案。

 

示例代码2:计算斐波那契数列

import java.util.concurrent.RecursiveTask;

public class FibonacciCalculator extends RecursiveTask<Long> {
    private static final int THRESHOLD = 20; // 分解阈值
    private final int n;

    public FibonacciCalculator(int n) {
        this.n = n;
    }

    @Override
    protected Long compute() {
        if (n <= 1) {
            return (long) n;
        } else if (n <= THRESHOLD) {
            // 对于较小的 n,直接递归计算
            return fibonacci(n);
        } else {
            // 对于较大的 n,使用 Fork/Join 框架进行并行计算
            FibonacciCalculator leftTask = new FibonacciCalculator(n - 1);
            FibonacciCalculator rightTask = new FibonacciCalculator(n - 2);
            leftTask.fork(); // 异步执行左子任务
            long rightResult = rightTask.compute(); // 同步执行右子任务并等待结果
            long leftResult = leftTask.join(); // 等待左子任务完成并获取结果
            return leftResult + rightResult; // 合并结果
        }
    }

    private long fibonacci(int n) {
        if (n <= 1) {
            return (long) n;
        } else {
            return fibonacci(n - 1) + fibonacci(n - 2);
        }
    }

    public static void main(String[] args) {
        int n = 40; // 计算第 40 个斐波那契数
        ForkJoinPool pool = new ForkJoinPool();
        FibonacciCalculator task = new FibonacciCalculator(n);
        long result = pool.invoke(task);
        System.out.println("Fibonacci(" + n + ") = " + result);
    }
}

代码解析

  1. FibonacciCalculator 类:继承自 RecursiveTask,表示一个有返回值的任务。
  2. 构造函数:初始化任务的参数,即要计算的斐波那契数列的项数 n。
  3. compute() 方法
    • 首先检查 n 是否小于或等于 1,如果是,则直接返回 n。
    • 如果 n 小于或等于阈值 THRESHOLD,则直接使用递归方法 fibonacci(n) 计算结果。
    • 如果 n 较大,则将任务分解为两个子任务,分别计算 fibonacci(n-1) 和 fibonacci(n-2)。
    • 使用 fork() 方法异步执行左子任务,使用 compute() 方法同步执行右子任务并等待结果。
    • 使用 join() 方法等待左子任务完成并获取结果。
    • 最后,将两个子任务的结果相加,得到最终结果。
  4. fibonacci 方法:传统的递归方法,用于计算斐波那契数列的值。

解释

  • 分解阈值:在 compute 方法中,我们设置了一个分解阈值 THRESHOLD。如果 n 小于或等于这个阈值,我们直接使用递归方法计算结果,避免过多的并行任务开销。对于较大的 n,我们使用 Fork/Join 框架进行并行计算。
  • 异步和同步执行:fork() 方法用于异步执行子任务,compute() 方法用于同步执行子任务并等待结果,join() 方法用于等待异步子任务完成并获取结果。
  • 并行计算:通过将任务分解为多个子任务并并行执行,可以显著提高计算斐波那契数列的效率,特别是对于较大的 n。

Fork/Join 框架特别适合于那些可以被有效分解为多个子任务的应用场景,如大数据处理、图像处理等。正确地利用 Fork/Join 框架可以显著提升程序的性能和响应速度。

欢迎大家在评论区讨论~