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

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

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

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

Ответ 1

Fork-join позволяет легко выполнять задания разделения и выполнения, которые необходимо выполнить вручную, если вы хотите выполнить его в ExecutorService. На практике ExecutorService обычно используется для обработки многих независимых запросов (aka transaction) одновременно и fork-join, когда вы хотите ускорить одно согласованное задание.

Ответ 2

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

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

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


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

Ответ 3

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

Ответ 4

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

В структуре Fork-Join есть две характеристики:

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

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

Ответ 5

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, он создаст один поток для каждого задания и не будет очереди ожидания.

Ответ 6

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

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

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