В чому різниця між 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]]