Java Fork/Join vs ExecutorService - когда использовать какой?

Я только что закончил читать этот пост: в чем преимущество Java-5 ThreadPoolExecutor перед Java-7 ForkJoinPool? и чувствовал, что ответ не достаточно прямой.

Можете ли вы объяснить простым языком и примерами, каковы компромиссы между платформой Fork-Join в Java 7 и более старыми решениями?

Я также читал хит #1 Google по теме Java. Совет: когда использовать ForkJoinPool против ExecutorService с https://www.javaworld.com/, но статья не отвечает на заглавный вопрос, когда речь идет в основном о различиях API...

6 ответов

Решение

Fork-join позволяет вам легко выполнять задания "разделяй и властвуй", которые должны быть реализованы вручную, если вы хотите выполнить их в ExecutorService. На практике ExecutorService обычно используется для одновременной обработки большого количества независимых запросов (транзакций), а также форк-соединения, когда вы хотите ускорить выполнение одной согласованной работы.

Fork-join особенно хорош для рекурсивных задач, когда задача включает выполнение подзадач и последующую обработку их результатов. (Обычно это называется "разделяй и властвуй" ... но это не раскрывает основных характеристик.)

Если вы попытаетесь решить рекурсивную проблему, подобную этой, используя обычную многопоточность (например, через ExecutorService), вы получите связанные потоки, ожидающие, пока другие потоки доставят им результаты.

С другой стороны, если проблема не имеет этих характеристик, использование fork-join не принесет никакой реальной выгоды.


Вот статья "Java Tips", которая более подробно рассматривается:

Java 8 предоставляет еще один API в Executors

static ExecutorService  newWorkStealingPool()

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

С добавлением этого API, Executors предоставляет различные типы опций ExecutorService.

В зависимости от ваших требований, вы можете выбрать один из них или можете поискать ThreadPoolExecutor, который обеспечивает лучший контроль над размером очереди ограниченных задач, RejectedExecutionHandler механизмы.

  1. static ExecutorService newFixedThreadPool(int nThreads)

    Создает пул потоков, который повторно использует фиксированное число потоков, работающих в общей неограниченной очереди.

  2. static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

    Создает пул потоков, который может запланировать выполнение команд после заданной задержки или периодическое выполнение.

  3. static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)

    Создает пул потоков, который создает новые потоки по мере необходимости, но будет повторно использовать ранее созданные потоки, когда они доступны, и использует предоставленный ThreadFactory для создания новых потоков, когда это необходимо.

  4. static ExecutorService newWorkStealingPool(int parallelism)

    Создает пул потоков, который поддерживает достаточное количество потоков для поддержки заданного уровня параллелизма и может использовать несколько очередей для уменьшения конкуренции.

Каждый из этих API предназначен для удовлетворения бизнес-потребностей вашего приложения. Какой из них использовать, зависит от вашего варианта использования.

например

  1. Если вы хотите обработать все представленные задачи в порядке поступления, просто используйте newFixedThreadPool(1)

  2. Если вы хотите оптимизировать производительность больших вычислений рекурсивных задач, используйте ForkJoinPool или же newWorkStealingPool

  3. Если вы хотите выполнять некоторые задачи периодически или в определенное время в будущем, используйте newScheduledThreadPool

Взгляните на еще одну хорошую статью PeterLawrey на ExecutorService случаи применения.

Связанный вопрос SE:

Java Форк / Объединить пул, ExecutorService и CountDownLatch

Брайан Гетц лучше всего описывает ситуацию: https://www.ibm.com/developerworks/library/j-jtp11137/index.html

Использование обычных пулов потоков для реализации fork-join также является сложной задачей, поскольку задачи fork-join проводят большую часть своей жизни в ожидании других задач. Это поведение является рецептом для тупиковой блокировки голодания потока, если параметры не были тщательно выбраны, чтобы ограничить число созданных задач или сам пул не ограничен. Обычные пулы потоков предназначены для задач, которые не зависят друг от друга, а также разработаны с учетом потенциально блокирующих, грубых задач - решения с разветвлением не дают ни того, ни другого.

Я рекомендую прочитать весь пост, так как в нем есть хороший пример того, почему вы хотите использовать пул fork-join. Он был написан до того, как ForkJoinPool стал официальным, поэтому coInvoke() метод, к которому он обращается, стал invokeAll(),

Фреймворк Fork-Join является расширением фреймворка Executor, в частности для решения проблем "ожидания" в рекурсивных многопоточных программах. Фактически, все новые классы платформы Fork-Join расширяются от существующих классов платформы Executor.

Есть две характеристики, центральные для структуры Fork-Join

  • Воровство работы (Свободный поток крадет работу из потока, задачи которого поставлены в очередь больше, чем он может обработать в настоящее время)
  • Возможность рекурсивного разложения задач и сбора результатов. (По-видимому, это требование должно было появиться вместе с концепцией понятия параллельной обработки... но отсутствовала надежная структура реализации в Java до Java 7)

Если потребности в параллельной обработке строго рекурсивны, нет другого выбора, кроме как перейти к Fork-Join, в противном случае следует использовать среду executor или Fork-Join, хотя можно сказать, что Fork-Join лучше использует ресурсы из-за незанятых потоков "украсть" некоторые задачи из более загруженных потоков.

Fork Join является реализацией ExecuterService. Основное отличие состоит в том, что эта реализация создает рабочий пул DEQUE. Где задание вставлено с одной стороны, но снято с любой стороны. Это означает, что если вы создали new ForkJoinPool() он будет искать доступный процессор и создавать столько рабочих потоков. Затем он равномерно распределяет нагрузку по каждому потоку. Но если один поток работает медленно, а другие быстро, они выберут задачу из медленного потока. с обратной стороны. Следующие шаги лучше проиллюстрируют кражу.

Этап 1 (изначально):
W1 -> 5,4,3,2,1
W2 -> 10,9,8,7,6

Этап 2:
W1 -> 5,4
W2 -> 10,9,8,7,

Этап 3:
W1 -> 10,5,4
W2 -> 9,8,7,

Принимая во внимание, что служба Executor создает запрашиваемый номер потока и применяет блокирующую очередь для хранения всех оставшихся ожидающих задач. Если вы использовали cachedExecuterService, он создаст отдельный поток для каждого задания, и очереди ожидания не будет.

Другие вопросы по тегам