Fork/Join Framework в Java 7

  • Tutorial
Какое-то время назад я делал обзор нововведений в Java 7, и, среди прочего, грозился по некоторым из новшеств пройтись более конкретно. Прошло довольно много времени, в Java 7 успели даже найти серьёзный дефект, но наконец настал тот момент, когда уже пора выполнить своё обещание. Потому под катом вы можете найти описание и пример использования новой реализации ExecutorService под названием ForkJoinPool. Эта реализация разработана специально для упрощения распараллеливания рекурсивных задач и решает проблему с тем, что пока выполняется под-задача, поток, который её породил, не может быть использован.

Краткое описание используемых типов

ForkJoinTask

Это абстрактный класс, который является в каком-то смысле легковесным аналогом потока. Суть в том, что благодаря ForkJoinPool, который будет рассмотрен ниже, можно в небольшом количестве потоков выполнить существенно большее число задач. Это достигается путём так называемого work-stealing'а, когда спящая задача на самом деле не спит, а выполняет другие задачи. У этого класса есть много интересных методов, но мы остановимся лишь на двух: fork(), который производит асинхронный запуск задачи и join(), который дожидается выполнения задачи и возвращает результат её выполнения. Более подробное описание всех методов можно почитать в официальной документации.

RecursiveAction и RecursiveTask

Сам по себе этот класс практически не используется, потому что для подавляющего большинства задач уже есть готовые более конкретные реализации: RecursiveAction на случай, если никакого значения посчитать не нужно, а нужно лишь выполнить некоторое действие, и RecursiveTask, когда всё же нужно что-то вернуть. Как вы могли заметить, эти два класса аналогичны уже существующим Runnable и Callable.

ForkJoinPool

В этом классе как раз и реализована хитрая логика по распределению нагрузки между реальными потоками. В принципе, снаружи он выглядит практически как замый обычный пул потоков, и особенностей в использовании нет.

Пример использования фреймворка


Придумаем задачу: пусть у нас есть некоторое дерево, у которого в узлах записаны числа. Требуется посчитать сумму всех таких чисел. Для улучшения взаимопонимания приведу интерфейс узла дерева:
public interface Node {
    Collection<Node> getChildren();
    
    long getValue();
}

Теперь опишем рекурсивную задачу, считающую в много потоков необходимую сумму, унаследовавшись от RecursiveTask. Нам нужно лишь переопределить метод compute и грамотно воспользоваться методами fork() и join:
public class ValueSumCounter extends RecursiveTask<Long>{
    private final Node node;
    
    public ValueSumCounter(Node node) {
        this.node = node;
    }

    @Override
    protected Long compute() {
        long sum = node.getValue();
        List<ValueSumCounter> subTasks = new LinkedList<>();
        
        for(Node child : node.getChildren()) {
            ValueSumCounter task = new ValueSumCounter(child);
            task.fork(); // запустим асинхронно
            subTasks.add(task);
        }
        
        for(ValueSumCounter task : subTasks) {
            sum += task.join(); // дождёмся выполнения задачи и прибавим результат 
        }
        
        return sum;
    }
    
}

Всё просто, не так ли? Осталось лишь запустить это счастье в отдельном ForkJoinPool:
public static void main(String[] args) {
    Node root = getRootNode();
    new ForkJoinPool().invoke(new ValueSumCounter(root));
}

И готово, результат вычисления у вас в кармане!

Зачем мне это?


В первую очередь, писать так существенно удобнее и интуитивно-понятнее, чем было с ипользованием Future<T>. Более того, как я уже писал ранее, для выполнения fork-нутой задачи вовсе не обязателен выделенный настоящий поток. Напротив, активно используются уже существующие потоки, которые в текущий момент находятся в join-е. Это, очевидно, даёт существенный прирост к производительности. Бенчмарк я проводить не стал, поскольку уверен, что за меня это уже сделали инженеры в Oracle.

Успехов в ваших изысканиях с Java 7 :)
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 11

    +1
    А List subTasks на момент второго цикла пустой)
      0
      И правда. Пофиксил, спасибо)
      +2
      Люк, это твоя вилка…
      • UFO just landed and posted this here
      • UFO just landed and posted this here
          +2
          new ForkJoinPool().invoke(root);

          Я что-то не догоняю как ForkJoinPool поймет какой именно таск выполнять с этой нодой?
            0
            oops, исправил, спасибо.
            0
            А в каком порядке выполняются задачи? Очередь?
              0
              Если я правильно понял, то по умолчанию LIFO порядок. Можно изменить на FIFO, если задать asyncMode равным false в конструкторе ForkJoinPool
                0
                Проверил на скорую руку, что для рассматриваемой задачи, если дерево представляет собой вырожденный случай списка, глубиной скажем 1 миллион, то приведенный код выбрасывает StackOverflowError. Немного обидно получается. Нельзя ли как то справить ситуацию?
                  0
                  GoodGreenTea Надо форк делать не на каждый элемент, а рекурсивно резать список пополам, до некоторого порогового значения количества элементов списка, когда сумму быстрее посчитать в том же потоке

                  Only users with full accounts can post comments. Log in, please.