В чем преимущество 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 предназначен для нескольких независимых, сгенерированных извне, долгих, иногда блокирующих задач

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

4 ответа

Решение

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

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

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

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

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

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

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

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

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

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

НАСКОЛЬКО МНЕ ИЗВЕСТНО, ForkJoinPool лучше всего работает, если у вас большой кусок работы, и вы хотите, чтобы он разбился автоматически. ThreadPoolExecutor это лучший выбор, если вы знаете, как вы хотите, чтобы работа была разбита. По этой причине я склонен использовать последнее, потому что я определил, как я хочу, чтобы работа была разбита. Как таковой его не для всех.

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

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

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()

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

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

  1. Вы можете контролировать размер очереди задач в ThreadPoolExecutor в отличие от ForkJoinPool,
  2. Вы можете применить Политику Отказа, когда вы исчерпали свои возможности в отличие от ForkJoinPool

Мне нравятся эти две особенности в ThreadPoolExecutor который сохраняет здоровье системы в хорошем состоянии.

РЕДАКТИРОВАТЬ:

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

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