Какое преимущество Java-5 ThreadPoolExecutor над Java-7 ForkJoinPool?

Java 5 представила поддержку для выполнения асинхронной задачи пулом потоков в виде инфраструктуры Executor, чье сердце является пулом потоков, реализованным java.util.concurrent.ThreadPoolExecutor. Java 7 добавила альтернативный пул потоков в виде java.util.concurrent.ForkJoinPool.

Посмотрев на свой API, ForkJoinPool обеспечивает надстройку функциональности ThreadPoolExecutor в стандартных сценариях (хотя, строго говоря, ThreadPoolExecutor предлагает больше возможностей для настройки, чем ForkJoinPool). Добавив к этому замечание, что fork/join задачи кажутся более быстрыми (возможно, из-за планировщика кражи работы), нужно определенно меньше потоков (из-за операции неблокирующего соединения), может возникнуть впечатление, что ThreadPoolExecutor был заменен ForkJoinPool.

Но это действительно правильно? Весь материал, который я прочитал, похоже, сводится к довольно расплывчатому различию между двумя типами пулов потоков:

  • ForkJoinPool предназначен для многих, зависимых, заданных, коротких, вряд ли когда-либо блокирующих (то есть вычислительных) задач
  • ThreadPoolExecutor - это несколько, независимых, созданных извне, длинных, иногда блокирующих задач.

Является ли это различие правильным? Можем ли мы сказать что-то более конкретное об этом?

Ответы

Ответ 1

ThreadPool (TP) и ForkJoinPool (FJ) ориентированы на различные варианты использования. Основное различие заключается в количестве очередей, используемых различными исполнителями, которые решают, какие проблемы лучше подходят для любого исполнителя.

Исполнитель FJ имеет n (aka parallelism level) отдельные параллельные очереди (deques), в то время как исполнитель TP имеет только одну параллельную очередь (эти очереди /deques, возможно, пользовательские реализации, не соответствующие API-интерфейсам JDK Collections). В результате в сценариях, где вы создаете большое количество (обычно относительно коротких) задач, исполнитель FJ будет работать лучше, поскольку независимые очереди минимизируют параллельные операции, а нечастые перехваты помогут с балансировкой нагрузки. В TP из-за одиночной очереди будут выполняться параллельные операции каждый раз, когда работа будет удалена, и она будет действовать как относительное узкое место и ограничить производительность.

В отличие от этого, если относительно меньше длительных задач одиночная очередь в TP больше не является узким местом для производительности. Тем не менее, n-независимые очереди и относительно частые попытки кражи работы теперь станут узким местом в FJ, так как может быть много тщетных попыток украсть работу, которая добавит накладные расходы.

Кроме того, алгоритм поиска работы в FJ предполагает, что (более старые) задачи, украденные из deque, будут давать достаточно параллельных задач для уменьшения количества краж. Например. в quicksort или mergesort, где более старые задачи равны более крупным массивам, эти задачи будут генерировать больше задач и держать очередь непустой и уменьшать количество общих краж. Если это не так в данном приложении, то частые попытки кражи снова становятся узким местом. Это также отмечено в javadoc для ForkJoinPool:

этот класс предоставляет методы проверки состояния (например, getStealCount()) которые призваны помочь в разработке, настройке и мониторинге fork/join.

Ответ 2

Рекомендуемое чтение http://gee.cs.oswego.edu/dl/jsr166/dist/docs/ Из документов для ForkJoinPool:

A ForkJoinPool отличается от других видов ExecutorService главным образом в силу использования кражи работы: все потоки в пуле пытаются находить и выполнять задачи, представленные в пул и/или созданные другими активные задачи (в конечном счете, блокирование ожидания работы, если они не существуют). Это позволяет эффективно обрабатывать, когда большинство задач порождают другие подзадачи (как и большинство ForkJoinTasks), а также когда многие небольшие задачи представленный в пул от внешних клиентов. Особенно при настройке asyncMode - true в конструкторах, ForkJoinPools также может быть подходящий для использования с задачами в стиле событий, которые никогда не соединяются.

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

Структура fork join также позволяет кражу работы (использование Deque).

Эта статья хорошо читается.

Ответ 3

AFAIK, ForkJoinPool работает лучше всего, если вы выполняете большую работу и хотите, чтобы она автоматически разбилась. ThreadPoolExecutor - лучший выбор, если вы знаете, как вы хотите, чтобы работа была разрушена. По этой причине я склонен использовать последнее, потому что я определил, как я хочу, чтобы работа была разрушена. Таким образом, это не для каждого.

Не стоит ничего, что когда дело доходит до относительно случайных частей бизнес-логики, ThreadPoolExecutor сделает все, что вам нужно, поэтому зачем это усложнять, чем вам нужно.

Ответ 4

Сравним различия в конструкторах:

ThreadPoolExecutor

ThreadPoolExecutor(int corePoolSize, 
                   int maximumPoolSize, 
                   long keepAliveTime, 
                   TimeUnit unit, 
                   BlockingQueue<Runnable> workQueue, 
                   ThreadFactory threadFactory,
                   RejectedExecutionHandler handler)

ForkJoinPool

ForkJoinPool(int parallelism,
            ForkJoinPool.ForkJoinWorkerThreadFactory factory,
            Thread.UncaughtExceptionHandler handler,
            boolean asyncMode)

Единственное преимущество, которое я видел в ForkJoinPool: механизм кражи работы с помощью незанятых потоков.

Java 8 представила еще один API в Executors - newWorkStealingPool, чтобы создать пул кражи работы. Вам не нужно создавать RecursiveTask и RecursiveAction, но все же можете использовать ForkJoinPool.

public static ExecutorService newWorkStealingPool()

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

Преимущества ThreadPoolExecutor для ForkJoinPool:

  • Вы можете контролировать размер очереди задач в ThreadPoolExecutor в отличие от ForkJoinPool.
  • Вы можете применить политику отклонения, когда у вас закончилась ваша емкость, в отличие от ForkJoinPool

Мне нравятся эти две функции в ThreadPoolExecutor, которые сохраняют работоспособность системы в хорошем состоянии.

EDIT:

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