Какие типы Thread Pool существуют в Java?
Java предоставляет несколько готовых типов пулов потоков через фабрику Executors. Каждый тип оптимизирован для определённого сценария.
Junior уровень
Базовое понимание
Java предоставляет несколько готовых типов пулов потоков через фабрику Executors. Каждый тип оптимизирован для определённого сценария.
1. FixedThreadPool
Фиксированное количество потоков. Все задачи выполняются параллельно, остальные ждут в очереди. Выбирайте, когда знаете максимальную нагрузку заранее (например, обработка 10 файлов одновременно).
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
pool.submit(() -> {
// Максимум 10 задач выполняются одновременно
// Остальные 90 ждут в очереди
});
}
| Параметр | Значение |
|---|---|
| Размер пула | Фиксированный (10) |
| Очередь | Безразмерная (LinkedBlockingQueue) |
| Когда использовать | Известная, стабильная нагрузка |
2. CachedThreadPool
Создаёт новые потоки по необходимости, переиспользует свободные.
ExecutorService pool = Executors.newCachedThreadPool();
pool.submit(() -> {
// Если нет свободных потоков — создаётся новый
// Если поток свободен 60 секунд — уничтожается
});
| Параметр | Значение |
|---|---|
| Размер пула | 0 до Integer.MAX_VALUE (теоретически ~2 млрд, на практике ограничено памятью ОС — обычно 32K потоков максимум на Linux) |
| Очередь | SynchronousQueue (ёмкость 0) |
| Когда использовать | Много коротких задач |
3. SingleThreadExecutor
Ровно один поток. Задачи выполняются строго последовательно.
ExecutorService pool = Executors.newSingleThreadExecutor();
pool.submit(() -> System.out.println("Задача 1"));
pool.submit(() -> System.out.println("Задача 2"));
// Всегда: "Задача 1" → "Задача 2"
| Параметр | Значение |
|---|---|
| Размер пула | 1 |
| Очередь | Безразмерная |
| Когда использовать | Последовательное выполнение |
4. ScheduledThreadPool
Планирование задач с задержкой или периодически.
ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);
// Выполнить через 10 секунд
pool.schedule(() -> System.out.println("Delayed"), 10, TimeUnit.SECONDS);
// Выполнять каждые 5 секунд
pool.scheduleAtFixedRate(() -> System.out.println("Periodic"), 0, 5, TimeUnit.SECONDS);
| Параметр | Значение |
|---|---|
| Размер пула | Фиксированный |
| Очередь | DelayedWorkQueue |
| Когда использовать | Периодические задачи |
5. WorkStealingPool (Java 8+)
Примечание: в Java 21+ с виртуальными потоками (Project Loom) для I/O-bound задач предпочтительнее
Executors.newVirtualThreadPerTaskExecutor(). Виртуальные потоки стоят ~KB вместо ~1MB и могут создаваться миллионами.
Использует ForkJoinPool с алгоритмом work-stealing.
ExecutorService pool = Executors.newWorkStealingPool();
// По умолчанию: число потоков = число ядер CPU
| Параметр | Значение |
|---|---|
| Размер пула | Число ядер CPU |
| Алгоритм | Work-stealing |
| Когда использовать | Распараллеливание вычислений |
Сравнительная таблица
| Тип | Размер | Очередь | Лучше для |
|---|---|---|---|
| FixedThreadPool | Фиксированный | Безразмерная | Стабильная нагрузка |
| CachedThreadPool | Динамический | SynchronousQueue | Короткие задачи |
| SingleThreadExecutor | 1 | Безразмерная | Последовательность |
| ScheduledThreadPool | Фиксированный | DelayedWorkQueue | Расписание |
| WorkStealingPool | CPU cores | Work-stealing | Вычисления |
Middle уровень
FixedThreadPool — детали
// Внутренняя реализация
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(
nThreads, // corePoolSize
nThreads, // maximumPoolSize
0L, TimeUnit.MILLISECONDS, // keepAliveTime
new LinkedBlockingQueue<Runnable>() // БЕЗРАЗМЕРНАЯ очередь
);
}
Опасность: Если задачи приходят быстрее, чем обрабатываются, очередь растёт до бесконечности → OutOfMemoryError.
Решение: Создавайте вручную с ограниченной очередью:
ExecutorService safePool = new ThreadPoolExecutor(
10, 10, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1000) // Лимит 1000 задач
);
CachedThreadPool — детали
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(
0, // corePoolSize
Integer.MAX_VALUE, // maximumPoolSize — нет лимита!
60L, TimeUnit.SECONDS, // Потоки умирают через 60s
new SynchronousQueue<Runnable>() // Ёмкость = 0
);
}
Опасность: При всплеске нагрузки может создать тысячи потоков → OutOfMemoryError: unable to create new native thread.
SingleThreadExecutor — детали
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(
new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()
)
);
}
Отличие от newFixedThreadPool(1): гарантирует, что настройки нельзя изменить (инкапсуляция).
ScheduledThreadPool — детали
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3);
// Однократное выполнение через задержку
scheduler.schedule(task, 10, TimeUnit.SECONDS);
// Фиксированная частота (от начала предыдущего запуска)
scheduler.scheduleAtFixedRate(task, 0, 5, TimeUnit.SECONDS);
// Фиксированная задержка (от конца предыдущего запуска)
scheduler.scheduleWithFixedDelay(task, 0, 5, TimeUnit.SECONDS);
// scheduleAtFixedRate: если задача длится 7 секунд, а период 5с —
// следующая запустится НЕМЕДЛЕННО после завершения (пропуск)
// scheduleWithFixedDelay: подождёт 5 секунд ПОСЛЕ завершения предыдущей
WorkStealingPool — детали
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool(
Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, // UncaughtExceptionHandler
true // asyncMode
);
}
Алгоритм work-stealing:
- У каждого потока своя очередь задач
- Если поток закончил свои задачи — “крадёт” из хвоста очереди другого потока
- Отлично для неравномерных задач, потому что свободный поток не простаивает, а «крадёт» непроработанные задачи у занятых потоков (work-stealing алгоритм).
Какой пул НЕ использовать
- НЕ CachedThreadPool для долгих задач → thread explosion (тысячи потоков)
- НЕ FixedThreadPool с LinkedBlockingQueue для непредсказуемой нагрузки → OOM (очередь растёт бесконечно)
- НЕ SingleThreadExecutor для CPU-bound параллелизма → один поток, нет параллелизма
- НЕ WorkStealingPool для I/O-bound → блокировка потока блокирует work-stealing
Senior уровень
Когда Executors не подходят
Senior-разработчики часто избегают методов Executors.* и создают ThreadPoolExecutor напрямую.
Почему:
| Проблема | Executors.* | ThreadPoolExecutor вручную |
|---|---|---|
| Лимит очереди | Нет (OOM риск) | Есть (ArrayBlockingQueue) |
| RejectedExecutionHandler | AbortPolicy | Любой (CallerRunsPolicy) |
| ThreadFactory | Default | Кастомный (имена, приоритеты) |
| keepAliveTime | Фиксированный | Настраиваемый |
Выбор пула в зависимости от задачи
| Тип задачи | Рекомендуемый пул | Формула |
|---|---|---|
| CPU-bound (расчёты) | FixedThreadPool | N или N+1 ядер |
| I/O-bound (БД, API) | FixedThreadPool или кастомный | N * (1 + W/S) |
| Короткие задачи | CachedThreadPool (с лимитом!) | maxPoolSize ограничен |
| Рекурсия / Стримы | ForkJoinPool | N ядер |
| Фоновые задачи | ScheduledThreadPool | Зависит от частоты |
Формула sizing для смешанных нагрузок
Threads = Number of Cores * Target CPU Utilization * (1 + Wait time / Service time)
Пример:
- 8 ядер CPU
- Целевая утилизация: 80% (0.8)
- 90% времени ждём (W/S = 9)
Threads = 8 * 0.8 * (1 + 9) = 8 * 0.8 * 10 = 64 потока
Thread Starvation
I/O-bound пул слишком мал (5 потоков):
→ Все 5 потоков ждут ответа от БД
→ Новые запросы ждут в очереди
→ Система "встала"
Решение: увеличить размер пула для I/O-bound задач
Sizing Formula в коде
public class PoolSizer {
public static int calculatePoolSize(int cores, double targetUtilization,
double waitTime, double serviceTime) {
return (int) Math.ceil(
cores * targetUtilization * (1 + waitTime / serviceTime)
);
}
public static void main(String[] args) {
int cores = Runtime.getRuntime().availableProcessors();
int poolSize = calculatePoolSize(cores, 0.8, 90, 10);
// 8 * 0.8 * (1 + 90/10) = 64
System.out.println("Recommended pool size: " + poolSize);
}
}
Диагностика
// Мониторинг пула
ThreadPoolExecutor executor = ...;
// Критические метрики
executor.getActiveCount(); // Сколько потоков работают
executor.getQueue().size(); // Растущая очередь = проблема
executor.getCompletedTaskCount(); // Прогресс
executor.getPoolSize(); // Текущий размер
Best Practices
- Избегайте Executors.newCachedThreadPool() в production — используйте кастомный с лимитом
- Ограничивайте очередь — ArrayBlockingQueue вместо LinkedBlockingQueue
- Формула sizing — CPU-bound: N+1, I/O-bound: N * (1 + W/S)
- WorkStealingPool — для рекурсивных задач и parallel streams
- ScheduledThreadPool — для периодических задач (cron-like)
- Мониторьте queue.size() — растущая очередь = деградация
- Кастомный ThreadFactory — именование потоков для отладки
🎯 Шпаргалка для интервью
Обязательно знать:
- 5 типов пулов: FixedThreadPool (фиксированный размер), CachedThreadPool (динамический), SingleThreadExecutor (1 поток), ScheduledThreadPool (по расписанию), WorkStealingPool (work-stealing алгоритм)
- FixedThreadPool использует LinkedBlockingQueue без лимита — риск OOM при непредсказуемой нагрузке
- CachedThreadPool: maximumPoolSize = Integer.MAX_VALUE, SynchronousQueue (ёмкость 0) — риск thread explosion
- WorkStealingPool = ForkJoinPool с asyncMode=true, каждый поток имеет свою deque-очередь
- scheduleAtFixedRate — от начала предыдущего запуска (может пропустить если задача длинная), scheduleWithFixedDelay — от конца
- SingleThreadExecutor гарантирует инкапсуляцию — нельзя изменить настройки, в отличие от newFixedThreadPool(1)
- В production избегайте Executors.* — создавайте ThreadPoolExecutor вручную с ограниченной очередью и RejectedExecutionHandler
- Формула sizing: CPU-bound = N+1, I/O-bound = N * (1 + W/S), смешанная = N * util * (1 + W/S)
Частые уточняющие вопросы:
- Чем FixedThreadPool отличается от SingleThreadExecutor? — FixedThreadPool можно перенастроить, SingleThreadExecutor — инкапсулирован (FinalizableDelegatedExecutorService)
- Почему CachedThreadPool опасен в production? — При всплеске нагрузки создаёт тысячи потоков → unable to create new native thread
- Как работает work-stealing? — У каждого потока своя deque; свободный поток «крадёт» задачи из хвоста чужой deque (FIFO), владелец берёт с головы (LIFO)
- Что выбрать для I/O-bound задач? — FixedThreadPool с формулой N * (1 + W/S) или виртуальные потоки (Java 21+)
Красные флаги (НЕ говорить):
- “CachedThreadPool — мой выбор для production” — опасен без ограничений, нужен maxPoolSize
- “FixedThreadPool с LinkedBlockingQueue безопасен” — безлимитная очередь = OOM
- “WorkStealingPool хорош для I/O” — нет, блокировка потока блокирует work-stealing
- “scheduleAtFixedRate гарантирует интервал” — нет, если задача длиннее периода — следующий запустится немедленно
Связанные темы:
- [[12. Что такое пул потоков (Thread Pool)]]
- [[15. Что делает ExecutorService]]
- [[16. В чём разница между Executors.newFixedThreadPool() и newCachedThreadPool()]]
- [[17. Что такое ForkJoinPool]]