Вопрос 15 · Раздел 9

В чём разница между Executors.newFixedThreadPool() и newCachedThreadPool()?

Оба пула создают ExecutorService, но их поведение радикально различается.

Версии по языкам: English Russian Ukrainian

Junior уровень

Базовое понимание

Оба пула создают ExecutorService, но их поведение радикально различается.

newFixedThreadPool

ExecutorService pool = Executors.newFixedThreadPool(10);
  • Фиксированное количество потоков (10)
  • Задачи ждут в очереди, если все потоки заняты
  • Потоки не умирают — всегда готовы к работе

newCachedThreadPool

ExecutorService pool = Executors.newCachedThreadPool();
  • Динамическое количество потоков (0 до бесконечности)
  • Каждая задача сразу получает поток (новый или свободный)
  • Свободные потоки умирают через 60 секунд

Сравнение

Характеристика FixedThreadPool CachedThreadPool
Размер пула Фиксированный Динамический
Очередь LinkedBlockingQueue (безлимитная) SynchronousQueue (ёмкость 0)
Поток без работы Живёт вечно Умирает через 60s
Лучше для Равномерная нагрузка Взрывная нагрузка

Middle уровень

Конфигурация под капотом

newFixedThreadPool

new ThreadPoolExecutor(
    n,                    // corePoolSize = n
    n,                    // maximumPoolSize = n
    0L,                   // keepAliveTime = 0
    TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>() // БЕЗЛИМИТНАЯ очередь
);

newCachedThreadPool

new ThreadPoolExecutor(
    0,                    // corePoolSize = 0
    Integer.MAX_VALUE,    // maximumPoolSize = бесконечность!
    60L,                  // keepAliveTime = 60 секунд
    TimeUnit.SECONDS,
    new SynchronousQueue<Runnable>() // Ёмкость = 0
);

Как передаются задачи

FixedThreadPool: Пассивная очередь

Задача → Очередь (LinkedBlockingQueue) → Свободный поток забирает
         [1] [2] [3] [4] [5] ... [1000] ... [∞]

Все задачи копятся в очереди. Потоки обрабатывают по одной.

CachedThreadPool: Передача из рук в руки

Задача → SynchronousQueue → НЕМЕДЛЕННО свободному потоку
                           → Или создаётся новый поток

SynchronousQueue имеет ёмкость 0. Каждая задача ОБЯЗАНА быть передана потоку немедленно.

Когда что использовать

Сценарий FixedThreadPool CachedThreadPool
Веб-сервер (постоянный трафик) Да Нет
Пакетная обработка Да Нет
Короткие HTTP-запросы Нет Да
Фоновые задачи (нечасто) Нет Да

Senior уровень

Опасности FixedThreadPool

Проблема: Безлимитная очередь → OutOfMemoryError

ExecutorService pool = Executors.newFixedThreadPool(10);

for (int i = 0; i < 10_000_000; i++) {
    pool.submit(() -> {
        Thread.sleep(1000); // Медленная задача
    });
}
// Очередь: 10_000_000 объектов Runnable → OutOfMemoryError!

Решение: Ограниченная очередь

ExecutorService safePool = new ThreadPoolExecutor(
    10, 10, 0L, MILLISECONDS,
    new ArrayBlockingQueue<>(10000), // Лимит!
    new ThreadPoolExecutor.CallerRunsPolicy() // Back-pressure
);

Опасности CachedThreadPool

Проблема: Thread explosion → OutOfMemoryError: unable to create new native thread

ExecutorService pool = Executors.newCachedThreadPool();

for (int i = 0; i < 100_000; i++) {
    pool.submit(() -> {
        Thread.sleep(5000); // Долгая задача
    });
}
// Создаст 100,000 потоков! ОС не выдержит → OOM

Решение: Ограничить maximumPoolSize

ExecutorService safeCached = new ThreadPoolExecutor(
    0, 200,               // Максимум 200 потоков!
    60L, TimeUnit.SECONDS,
    new SynchronousQueue<>(),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

Сравнение механизмов (Advanced)

Характеристика FixedThreadPool CachedThreadPool
Ограничение ресурсов Жёсткое по потокам Нет (риск OOM по потокам)
Очередь Пассивная (накопительная) Активная (передача из рук в руки)
Простой потоков Потоки не умирают Потоки удаляются через минуту
Latency ~выше (задача стоит в очереди, пока освободится поток, +0-1000ms) ~ниже (сразу получает поток, ~0ms задержки)
Memory Стабильная Растёт при всплеске

Рекомендации для Production

В production Executors.newCachedThreadPool() опасен без ограничений, потому что при всплеске нагрузки создаёт тысячи потоков. Для коротких задач используйте кастомный ThreadPoolExecutor с maxPoolSize. В Java 21+ рассмотрите виртуальные потоки.

Почему:

  • Нарушает принцип предсказуемости
  • При всплеске нагрузки создаёт тысячи потоков
  • Может убить ОС или JVM

Как правильно:

// Безопасный аналог CachedThreadPool
ExecutorService safePool = new ThreadPoolExecutor(
    0,                      // corePoolSize
    200,                    // maximumPoolSize — жёсткий лимит!
    60L, TimeUnit.SECONDS,
    new SynchronousQueue<>(),
    new ThreadPoolExecutor.CallerRunsPolicy() // Back-pressure
);

Диагностика

VisualVM / JConsole

FixedThreadPool:    ──────────────── стабильная линия (10 потоков)
CachedThreadPool:   /\/\/\/\        "лес" — появляются и исчезают

Thread Dumps

jstack <pid>

CachedThreadPool проблемы:

"pool-1-thread-1001" #1010 RUNNABLE
"pool-1-thread-1002" #1011 RUNNABLE
...
// Сотни потоков, ожидающих на SynchronousQueue

Monitoring

// Порог зависит от размера задачи в памяти.
// Если Runnable = 1KB, то 1000 задач = 1MB.
// Если Runnable замыкает большие объекты — порог должен быть ниже.
int queueSize = ((ThreadPoolExecutor)pool).getQueue().size();
if (queueSize > 1000) {
    log.warn("Queue is growing! Possible OOM risk");
}

// CachedThreadPool — мониторим pool size
int poolSize = ((ThreadPoolExecutor)pool).getPoolSize();
if (poolSize > 100) {
    log.warn("Too many threads! Possible thread explosion");
}

Best Practices

Java 21+: Для коротких задач Executors.newVirtualThreadPerTaskExecutor() заменяет CachedThreadPool. Виртуальные потоки стоят ~KB (vs ~1MB у обычных) и могут создаваться миллионами без риска thread explosion.

  1. FixedThreadPool — для стабильной нагрузки с известным максимумом
  2. CachedThreadPool — ТОЛЬКО для коротких задач с ограничением maxPoolSize
  3. Всегда ограничивайте очередь — ArrayBlockingQueue с лимитом
  4. Всегда ограничивайте maxPoolSize — даже для CachedThreadPool
  5. CallerRunsPolicy — back-pressure при перегрузке
  6. Мониторьте queue.size() для FixedThreadPool
  7. Мониторьте pool size для CachedThreadPool
  8. Избегайте Executors.* в production — создавайте ThreadPoolExecutor напрямую

🎯 Шпаргалка для интервью

Обязательно знать:

  • FixedThreadPool: фиксированный размер, LinkedBlockingQueue без лимита, потоки не умирают — риск OOM от очереди
  • CachedThreadPool: 0..Integer.MAX_VALUE потоков, SynchronousQueue (ёмкость 0), потоки умирают через 60s — риск thread explosion
  • SynchronousQueue имеет ёмкость 0 — каждая задача ОБЯЗАНА быть передана потоку немедленно или создаётся новый
  • FixedThreadPool latency выше (задача ждёт в очереди), CachedThreadPool latency ниже (~0ms задержки)
  • FixedThreadPool — для стабильной равномерной нагрузки; CachedThreadPool — для коротких задач с взрывным трафиком
  • В production оба опасны: FixedThreadPool → OOM от очереди, CachedThreadPool → unable to create new native thread
  • Решение: кастомный ThreadPoolExecutor с ArrayBlockingQueue и maxPoolSize + CallerRunsPolicy для back-pressure
  • Java 21+: Executors.newVirtualThreadPerTaskExecutor() заменяет CachedThreadPool (~KB на поток vs ~1MB)

Частые уточняющие вопросы:

  • Почему FixedThreadPool может вызвать OOM? — LinkedBlockingQueue без лимита — если задачи приходят быстрее обработки, очередь растёт бесконечно
  • Почему CachedThreadPool может создать тысячи потоков? — SynchronousQueue не буферизует, каждый свободный слот → новый поток, maximumPoolSize = Integer.MAX_VALUE
  • Что такое back-pressure и как работает CallerRunsPolicy? — Задача выполняется в потоке-отправителе, он не может отправлять новые — автоматически замедляет источник
  • Как сделать безопасный аналог CachedThreadPool? — ThreadPoolExecutor с maxPoolSize=200 и CallerRunsPolicy

Красные флаги (НЕ говорить):

  • “CachedThreadPool — мой дефолт для любых задач” — опасен для долгих задач, thread explosion
  • “FixedThreadPool полностью безопасен” — нет, безлимитная очередь = OOM
  • “SynchronousQueue хранит задачи в очереди” — нет, ёмкость 0, только прямая передача
  • “Потоки в CachedThreadPool живут вечно” — нет, умирают через 60 секунд простоя

Связанные темы:

  • [[12. Что такое пул потоков (Thread Pool)]]
  • [[13. Какие типы Thread Pool существуют в Java]]
  • [[15. Что делает ExecutorService]]
  • [[17. Что такое ForkJoinPool]]