Які типи 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]]