Использование отдельных пулов потоков для обычных и запланированных задач

#java #multithreading #threadpool #threadpoolexecutor #scheduledexecutorservice

Вопрос:

Предположим , у меня уже есть широкое приложение ThreadPoolExecutor , и теперь мне также нужен ScheduledExecutorService . Должен ли я просто изменить реализацию ScheduledThreadPoolExecutor и использовать ее как для запланированных, так и для обычных задач? Или у меня должна ThreadPoolExecutor быть единица для обычных задач и отдельная ScheduledThreadPoolExecutor для запланированных задач? Хорошая ли идея использовать ScheduledThreadPoolExecutor его для обычных задач, или в его реализации есть что-то, что делает его полезным только для запланированных задач?

Я думаю, что один ScheduledThreadPoolExecutor из них позволит наилучшим образом использовать поток, в то время как это увеличивает риск голодания, если у нас много запланированных задач и мало обычных или наоборот.

Для большего контекста у меня есть старая кодовая база, которая все еще используется java.util.Timer для запланированных задач, и приложение ThreadPoolExecutor для обычных задач. Поскольку Timer это однопоточный, наши задачи времени обычно сразу же переносят вычисления в пул потоков, как только таймер их выполняет, чтобы не задерживать дальнейшие задачи времени. Что-то в этом роде:

 timer.schedule(new TimerTask() {
  public void run() {
    executor.execute(this::performWork);
  }
}, 1000);
 

До сих пор мы откладывали рефакторинг от Timer до ScheduledThreadPoolExecutor , но только сейчас я понимаю, что мы можем исключить конструкции, подобные приведенной выше, при использовании одного пула потоков для всех видов задач.

Ответ №1:

Я бы рекомендовал использовать два отдельных пула по двум причинам. Первая причина заключается в том, что с точки зрения приложения мне проще настраивать пулы, если они различны. Вероятно, легче предсказать количество потоков, необходимых для запланированных задач и для одноразовых задач, чем общую рабочую нагрузку. Вторая причина связана с API:

Хотя этот класс наследуется от ThreadPoolExecutor, некоторые унаследованные методы настройки для него бесполезны. В частности, поскольку он действует как пул фиксированного размера с использованием потоков corePoolSize и неограниченной очереди, настройки максимального размера пула не оказывают полезного эффекта. Кроме того, почти никогда не рекомендуется устанавливать значение corePoolSize равным нулю или использовать allowCoreThreadTimeOut, поскольку это может оставить пул без потоков для обработки задач, как только они получат право на выполнение.

Другими словами, он не совсем ведет себя как обычный ThreadPoolExecutor, и усилия по настройке могут иметь удивительные эффекты, включая голодание.

Комментарии:

1. Интересно! Что делать, если настройка окажется аналогичной или если на запланированные задачи будет очень мало нагрузки? Вы все еще рекомендовали бы два отдельных бассейна? В любом случае, я, вероятно, предложу два разных метода для получения соответствующего пула. Затем я могу позже изменить, указывают ли они на один и тот же пул или нет.

2. Вероятно, вам не нужен пул фиксированного размера для ваших обычных задач, так что да, рекомендация остается в силе.

3. Имеет смысл. Наверное, я неправильно истолковал тот факт, что Целое число. Значение MAX_VALUE передается maximumPoolSize суперконструктору, что действительно не имеет никакого эффекта.

4. И для полноты картины, что вы думаете о стратегии перенаправления фактического выполнения запланированной задачи в другой пул? Хорошо это, плохо или просто не стоит того? Если это хорошая идея, я мог бы создать оболочку ScheduledExecutorService , которая бы обрабатывала это прозрачно.

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