Объясните принцип работы фреймворка Fork/Join в Java.

«Объясните принцип работы фреймворка Fork/Join в Java.» — вопрос из категории Java Core, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Fork/Join — это фреймворк в Java (пакет java.util.concurrent), предназначенный для эффективного параллельного выполнения рекурсивных задач, которые можно разбивать на подзадачи (divide-and-conquer). Он реализован на основе work-stealing алгоритма.

Ключевые компоненты:

  1. ForkJoinPool: Специальный пул потоков-исполнителей. Потоки в нем могут "красть" задачи из очередей других потоков, что обеспечивает балансировку нагрузки.
  2. ForkJoinTask: Абстрактный базовый класс для задач. На практике используются его подклассы:
    • RecursiveAction — для задач, не возвращающих результат.
    • RecursiveTask<V> — для задач, возвращающих результат.

Принцип работы:

  1. Задача проверяет, достаточно ли она мала для непосредственного выполнения (базовый случай).
  2. Если нет, она разделяется (fork) на две или более подзадач, которые асинхронно запускаются в пуле.
  3. Подзадачи рекурсивно повторяют шаги 1-2.
  4. Результаты подзадаж объединяются (join).

Пример: вычисление суммы элементов массива с помощью RecursiveTask:

import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;

class SumTask extends RecursiveTask<Long> {
    private final int[] array;
    private final int start, end;
    private static final int THRESHOLD = 10_000; // Порог для прямого вычисления

    SumTask(int[] array, int start, int end) {
        this.array = array; this.start = start; this.end = end;
    }

    @Override
    protected Long compute() {
        if (end - start <= THRESHOLD) { // Базовый случай
            long sum = 0;
            for (int i = start; i < end; i++) sum += array[i];
            return sum;
        } else { // Разделение задачи
            int mid = (start + end) / 2;
            SumTask left = new SumTask(array, start, mid);
            SumTask right = new SumTask(array, mid, end);
            left.fork(); // Асинхронный запуск левой подзадачи
            long rightResult = right.compute(); // Синхронное вычисление правой
            long leftResult = left.join(); // Ожидание результата левой
            return leftResult + rightResult; // Объединение результатов
        }
    }
}

// Использование
int[] data = new int[100_000];
ForkJoinPool pool = new ForkJoinPool();
long result = pool.invoke(new SumTask(data, 0, data.length));

Почему Fork/Join эффективен? Work-stealing минимизирует время простоя потоков, что делает фреймворк идеальным для задач с высокой степенью рекурсивного параллелизма.