Рубрики
Без рубрики

Исполнители newCachedThreadPool() vs newFixedThreadPool()

Сравните реализации newCachedThreadPool() и newFixedThreadPool() и их варианты использования

Автор оригинала: Ali Dehghani.

1. Обзор

Когда дело доходит до реализации пула потоков, стандартная библиотека Java предоставляет множество вариантов на выбор. Фиксированные и кэшированные пулы потоков довольно распространены среди этих реализаций.

В этом уроке мы увидим, как пулы потоков работают под капотом, а затем сравним эти реализации и их варианты использования.

2. Кэшированный пул потоков

Давайте посмотрим, как Java создает кэшированный пул потоков, когда мы вызываем Executors.newCachedThreadPool() :

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

Кэшированные пулы потоков используют “синхронную передачу” для постановки новых задач в очередь. Основная идея синхронной передачи проста и в то же время интуитивно понятна: можно поставить элемент в очередь тогда и только тогда, когда другой поток принимает этот элемент одновременно. Другими словами, /SynchronousQueue не может содержать никаких задач вообще.

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

Кэшированный пул начинается с нулевых потоков и потенциально может вырасти до Integer.MAX_VALUE потоки. Практически единственным ограничением для пула кэшированных потоков являются доступные системные ресурсы.

Чтобы лучше управлять системными ресурсами, пулы кэшированных потоков удаляют потоки, которые остаются простаивающими в течение одной минуты.

2.1. Примеры использования

Конфигурация пула кэшированных потоков кэширует потоки (отсюда и название) в течение короткого периода времени, чтобы повторно использовать их для других задач. В результате это работает лучше всего, когда мы имеем дело с разумным количеством краткосрочных задач.

Ключ здесь – “разумный” и “недолговечный”. Чтобы прояснить этот момент, давайте оценим сценарий, в котором кэшированные пулы не подходят. Здесь мы представим миллион заданий, каждое из которых займет 100 микросекунд:

Callable task = () -> {
    long oneHundredMicroSeconds = 100_000;
    long startedAt = System.nanoTime();
    while (System.nanoTime() - startedAt <= oneHundredMicroSeconds);

    return "Done";
};

var cachedPool = Executors.newCachedThreadPool();
var tasks = IntStream.rangeClosed(1, 1_000_000).mapToObj(i -> task).collect(toList());
var result = cachedPool.invokeAll(tasks);

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

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

3. Фиксированный пул Потоков

Давайте посмотрим, как фиксированный поток пулы работают под капотом:

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

В отличие от пула кэшированных потоков, в этом пуле используется неограниченная очередь с фиксированным числом никогда не истекающих потоков . Поэтому вместо постоянно увеличивающегося числа потоков пул фиксированных потоков пытается выполнять входящие задачи с фиксированным количеством потоков . Когда все потоки будут заняты, исполнитель поставит новые задачи в очередь. Таким образом, у нас будет больше контроля над потреблением ресурсов нашей программы.

В результате фиксированные пулы потоков лучше подходят для задач с непредсказуемым временем выполнения.

4. Прискорбное Сходство

До сих пор мы только перечислили различия между кэшированными и фиксированными пулами потоков.

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

Давайте посмотрим, что происходит в реальном мире.

Кэшированные пулы потоков будут продолжать создавать все больше и больше потоков в экстремальных обстоятельствах, поэтому практически они никогда не достигнут точки насыщения . Аналогично, фиксированные пулы потоков будут продолжать добавлять все больше и больше задач в свою очередь. Следовательно, фиксированные пулы также никогда не достигнут точки насыщения .

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

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

var boundedQueue = new ArrayBlockingQueue(1000);
new ThreadPoolExecutor(10, 20, 60, SECONDS, boundedQueue, new AbortPolicy());

Здесь наш пул потоков может содержать до 20 потоков и может ставить в очередь только до 1000 задач. Кроме того, когда он больше не сможет принимать нагрузку, он просто выдаст исключение.

5. Заключение

В этом уроке мы заглянули в исходный код JDK, чтобы увидеть, как разные Executor s |/работают под капотом. Затем мы сравнили фиксированные и кэшированные пулы потоков и их варианты использования.

В конце концов, мы попытались решить проблему неконтролируемого потребления ресурсов этими пулами с помощью пользовательских пулов потоков.