FixedThreadPool против CachedThreadPool: меньшее из двух зол

У меня есть программа, которая порождает потоки (~ 5-150), которые выполняют кучу задач. Первоначально я использовал FixedThreadPool потому что этот похожий вопрос предполагал, что они лучше подходят для задач с более длительным сроком службы, и с моим очень ограниченным знанием многопоточности я считал средний срок службы потоков (несколько минут) "долгоживущим".

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

Испытывая их оба предварительно, кажется, нет никакой разницы, поэтому я склонен использовать CachedThreadPool только для того, чтобы избежать потерь. Однако означает ли срок службы потоков, что я должен вместо этого выбрать FixedThreadPool и просто иметь дело с неиспользованными потоками? Этот вопрос создает впечатление, что эти лишние темы не пропали даром, но я был бы признателен за разъяснения.

Ответ 1

A CachedThreadPool - это именно то, что вы должны использовать для своей ситуации, поскольку нет никаких негативных последствий для использования одного для длинных потоков. Комментарий в java-документе о CachedThreadPools, подходящий для коротких задач, просто указывает на то, что они особенно подходят для таких случаев, а не то, что они не могут или не должны использоваться для задач, связанных с длительными работами.

Далее, Executors.newCachedThreadPool и Executors.newFixedThreadPool оба поддерживаются одной и той же реализацией пула потоков (по крайней мере, в открытом JDK) только с разными параметрами. Различия просто являются минимальным количеством потоков, максимумом, временем уничтожения потока и типом очереди.

public static ExecutorService newFixedThreadPool(int nThreads) {
     return new ThreadPoolExecutor(nThreads, nThreads,
                                   0L, TimeUnit.MILLISECONDS,
                                   new LinkedBlockingQueue<Runnable>());
 }

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                 60L, TimeUnit.SECONDS,
                                 new SynchronousQueue<Runnable>());
}

У FixedThreadPool есть свои преимущества, когда вы действительно хотите работать с фиксированным количеством потоков, поскольку после этого вы можете отправлять любое количество задач службе-исполнителю, зная, что количество потоков будет поддерживаться на уровне вы указали. Если вы явно хотите увеличить количество потоков, то это не подходящий выбор.

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

Ответ 2

И FixedThreadPool и CachedThreadPool - это зло в сильно загруженных приложениях.

CachedThreadPool более опасен, чем FixedThreadPool

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

  1. Неограниченный характер очереди задач: это может вызвать нехватку памяти или высокую задержку
  2. Длительные потоки приведут к тому, что CachedThreadPool выйдет из-под контроля при создании потока

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

  1. Установите очередь задач как ограниченную очередь для лучшего контроля
  2. Иметь право RejectionHandler - ваши собственные обработчики RejectionHandler или Default, предоставляемые JDK
  3. Если у вас есть чем заняться до/после завершения задачи, переопределите beforeExecute(Thread, Runnable) и afterExecute(Runnable, Throwable)
  4. Переопределить ThreadFactory, если требуется настройка потока
  5. Размер пула управляющих потоков динамически во время выполнения (связанный вопрос SE: Dynamic Thread Pool)

Ответ 3

Итак, у меня есть программа, которая порождает потоки (~ 5-150), которые выполняют кучу задач.

Вы уверены, что понимаете, как потоки фактически обрабатываются вашей операционной системой и оборудованием по выбору? Как Java отображает потоки в потоки ОС, как отображает потоки в потоках процессора и т.д.? Я спрашиваю, потому что создание 150 потоков внутри ONE JRE имеет смысл только в том случае, если у вас есть массивные ядра ядра/потоки внизу, что, скорее всего, не так. В зависимости от используемой ОС и ОЗУ создание более чем n потоков может привести к тому, что ваша JRE будет прекращена из-за ошибок OOM. Таким образом, вы должны действительно различать потоки и работу, выполняемые этими потоками, сколько работы вы даже можете обрабатывать и т.д.

И что проблема с CachedThreadPool: не имеет смысла ставить в очередь длительную работу в потоках, которые на самом деле не могут выполняться, потому что у вас есть только 2 ядра процессора, способных обрабатывать эти потоки. Если вы закончите с 150 запланированными потоками, вы можете создать много лишних накладных расходов для планировщиков, используемых в Java и ОС, для одновременного их обработки. Это просто невозможно, если у вас только 2 ядра процессора, если только ваши потоки не ждут ввода/вывода или все такое время. Но даже в этом случае много потоков создадут много операций ввода-вывода...

И эта проблема не возникает при использовании FixedThreadPool, созданного с помощью, например, 2 + n, где n разумно мало, конечно, потому что с этим оборудованием и ресурсами ОС используются с гораздо меньшими накладными расходами для управления потоками, которые в любом случае не могут работать.