В чём разница между Executors.newFixedThreadPool() и newCachedThreadPool()?
Оба пула создают ExecutorService, но их поведение радикально различается.
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.
- FixedThreadPool — для стабильной нагрузки с известным максимумом
- CachedThreadPool — ТОЛЬКО для коротких задач с ограничением maxPoolSize
- Всегда ограничивайте очередь — ArrayBlockingQueue с лимитом
- Всегда ограничивайте maxPoolSize — даже для CachedThreadPool
- CallerRunsPolicy — back-pressure при перегрузке
- Мониторьте queue.size() для FixedThreadPool
- Мониторьте pool size для CachedThreadPool
- Избегайте 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]]