Що робить ExecutorService?
ExecutorService — це основний інтерфейс в Java для управління пулом потоків та виконання асинхронних задач.
Junior рівень
Базове розуміння
ExecutorService — це основний інтерфейс в Java для управління пулом потоків та виконання асинхронних задач.
Без ExecutorService: ви вручну створюєте потоки (new Thread().start()), контролюєте їх життєвий цикл та обробляєте помилки. З ExecutorService: ви лише надсилаєте задачі та отримуєте результати — управління потоками бере на себе фреймворк. Він відокремлює “що робити” (задачу) від “як і коли робити” (механізм виконання).
Простий приклад
// Створення ExecutorService
ExecutorService executor = Executors.newFixedThreadPool(3);
// Надсилання задачі
executor.submit(() -> {
System.out.println("Задачу виконано!");
});
// Завершення
executor.shutdown();
Основні способи надсилання задач
| Метод | Повертає | Винятки | Коли використовувати |
|---|---|---|---|
execute(Runnable) |
void | Вилітають у консоль | “Вистрілив і забув” |
submit(Runnable) |
Future<?> | Упаковані в ExecutionException — обгортка потрібна, щоб відрізнити помилку задачі від помилки самого ExecutorService. Оригінальний виняток доступний через e.getCause(). |
Потрібно відстежити завершення |
submit(Callable<T>) |
Future |
Упаковані в ExecutionException — обгортка потрібна, щоб відрізнити помилку задачі від помилки самого ExecutorService. Оригінальний виняток доступний через e.getCause(). |
Потрібен результат |
execute() vs submit()
// execute() — "вистрілив і забув"
executor.execute(() -> {
System.out.println("Виконуюсь...");
// Помилка тут — просто надрукується в System.err
});
// submit() — можна отримати результат і помилку
Future<?> future = executor.submit(() -> {
System.out.println("Виконуюсь...");
// Помилка тут — буде в future.get()
});
// Перевірка завершення
if (future.isDone()) {
System.out.println("Задачу завершено");
}
Закриття пулу
// Спосіб 1: Просте завершення
executor.shutdown(); // Перестаємо приймати нові задачі
executor.awaitTermination(60, TimeUnit.SECONDS); // Чекаємо до 60 секунд
// Спосіб 2: Примусове завершення
executor.shutdownNow(); // Перериває активні задачі
Middle рівень
Життєвий цикл ExecutorService
┌──────────┐ shutdown() ┌───────────┐ всі задачі ┌─────────┐
│ RUNNING │ ──────────────→ │ SHUTDOWN │ ────────────→ │ TIDYING │
│ │ │ │ │ │
│ при- │ shutdownNow() │ не при- │ │ │
| ймає │ ──────────────→ | ймає │ │ │
│ задачі │ │ задачі │ │ │
│ │ ←───────────── │ доробляє │ │ │
│ оброб- │ interrupted │ чергу │ │ │
│ ляє │ │ │ │ │
└──────────┘ └───────────┘ └────┬────┘
│ │
│ │ terminated()
│ ▼
│ ┌───────────┐
└────────────────────────────────────────────────│ TERMINATED│
└───────────┘
| Стан | Опис |
|---|---|
| RUNNING | Приймає та обробляє задачі |
| SHUTDOWN | Не приймає нові, доробляє чергу |
| STOP | Не приймає, перериває активні |
| TIDYING | Всі задачі завершено, потоки зупинено |
| TERMINATED | terminated() виконано |
submit(): Future та обробка помилок
// Callable — повертає результат
Future<String> future = executor.submit(() -> {
return "Результат";
});
// Отримання результату (блокує!)
String result = future.get();
// Отримання з таймаутом
try {
String result = future.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
future.cancel(true); // Скасовуємо задачу
}
// cancel(true) посилає задачі interrupt().
// Задача має сама перевірити Thread.interrupted() і завершитись.
// Якщо задача не обробляє переривання — вона продовжить працювати!
invokeAll() та invokeAny()
// invokeAll() — чекає ВСІ задачі
List<Callable<String>> tasks = List.of(
() -> "Task 1",
() -> "Task 2",
() -> "Task 3"
);
List<Future<String>> futures = executor.invokeAll(tasks);
// Блокує, поки всі не завершаться
// invokeAny() — чекає ПЕРШУ успішну задачу
String result = executor.invokeAny(tasks);
// Повертає результат першої завершеної задачі, решту скасовує
Правильне завершення пулу
public void shutdownGracefully(ExecutorService executor) {
executor.shutdown(); // 1. Перестаємо приймати
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { // 2. Чекаємо
executor.shutdownNow(); // 3. Примусово
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
System.err.println("Pool did not terminate");
}
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt(); // Відновлюємо флаг
}
}
submit() vs execute() — пастка з винятками
// execute() — виняток вилітає одразу
executor.execute(() -> {
throw new RuntimeException("Помилка!"); // → UncaughtExceptionHandler
});
// submit() — виняток "схований" у Future
Future<?> future = executor.submit(() -> {
throw new RuntimeException("Помилка!");
});
// Жодної помилки поки ви не викличете future.get()!
try {
future.get(); // → ExecutionException
} catch (ExecutionException e) {
System.out.println(e.getCause()); // → RuntimeException: Помилка!
}
Senior рівень
Деталі реалізації ThreadPoolExecutor (ctl, черги, ThreadFactory) — у файлі [[12. Що таке пул потоків (Thread Pool)]].
Thread Pool Leaks
// ПОГАНО: створення пулу всередині методу
public void process() {
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> doWork());
executor.shutdown();
// Якщо doWork() кидає виняток — shutdown() не викликається
// Потоки витікають!
}
// ГАРНО: пул — синглтон або управляється контейнером
@Component
public class TaskProcessor {
private final ExecutorService executor;
public TaskProcessor() {
this.executor = new ThreadPoolExecutor(...);
}
@PreDestroy
public void shutdown() {
shutdownGracefully(executor);
}
}
Correlation ID / Trace Context
// Проблема: Trace ID не пробрасується автоматично
executor.submit(() -> {
// MDC.get("traceId") = null!
});
// Рішення: обгортка
public class TracingExecutorService implements ExecutorService {
private final ExecutorService delegate;
public Future<?> submit(Runnable task) {
Map<String, String> mdcContext = MDC.getCopyOfContextMap();
return delegate.submit(() -> {
if (mdcContext != null) MDC.setContextMap(mdcContext);
try {
task.run();
} finally {
MDC.clear();
}
});
}
}
Діагностика
Метрики для моніторингу
// Виводимо в Prometheus:
executor.getActiveCount(); // Активних потоків
executor.getQueue().size(); // Розмір черги
executor.getCompletedTaskCount(); // Завершених задач
executor.getTaskCount(); | Всього задач
executor.getPoolSize(); // Розмір пулу
Future.isDone() / isCancelled()
Future<?> future = executor.submit(task);
// Без блокування
if (future.isDone()) {
System.out.println("Завершена");
}
if (future.isCancelled()) {
System.out.println("Скасована");
}
Best Practices
- Не створюйте ExecutorService всередині методу — витік потоків
- Використовуйте submit() — якщо потрібна обробка помилок
- Завжди викликайте shutdown() — у finally або @PreDestroy
- ArrayBlockingQueue — захист від OOM
- CallerRunsPolicy — back-pressure при перевантаженні
- Кастомний ThreadFactory — іменування потоків
- Trace ID propagation — для мікросервісів
- Моніторте queue.size() — черга, що росте = деградація
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- ExecutorService — інтерфейс управління пулом потоків: надсилання задач, отримання результатів, graceful shutdown
- execute(Runnable) = void, винятки вилітають у консоль; submit() = Future, винятки сховані в ExecutionException
- submit(Callable
) повертає Future — можна отримати результат та обробити помилку через get() - Життєвий цикл: RUNNING → SHUTDOWN (shutdown()) → STOP (shutdownNow()) → TIDYING → TERMINATED
- invokeAll() — чекає ВСІ задачі; invokeAny() — повертає результат ПЕРШОЇ успішної, решту скасовує
- Graceful shutdown: shutdown() → awaitTermination(timeout) → shutdownNow() → awaitTermination
- Future.get(timeout) кидає TimeoutException — при таймауті потрібно викликати future.cancel(true)
- ExecutorService має управлятися контейнером (Spring @Bean) або синглтоном, НЕ створюватися всередині методу
Часті уточнюючі запитання:
- У чому різниця execute() vs submit()? — execute «вистрілив і забув», submit дозволяє відстежити результат і помилку через Future
- Чому submit() не кидає виняток одразу? — Виняток упакований в ExecutionException і доступний тільки через future.get().getCause()
- Чим shutdown() відрізняється від shutdownNow()? — shutdown доробляє чергу, shutdownNow перериває активні задачі через interrupt()
- Як пробросити MDC/Trace ID в ExecutorService? — Обгортка: MDC.getCopyOfContextMap() перед submit, MDC.setContextMap() всередині задачі
Червоні прапорці (НЕ говорити):
- “Створюю ExecutorService всередині методу” — витік потоків, якщо shutdown() не викликається
- “submit() одразу покаже виняток” — ні, потрібно викликати future.get()
- “shutdownNow() гарантовано зупиняє всі задачі” — ні, лише посилає interrupt(), задачі мають самі обробити
- “cancel(true) миттєво вбиває задачу” — ні, лише виставляє interrupted flag, задача має перевірити
Пов’язані теми:
- [[12. Що таке пул потоків (Thread Pool)]]
- [[13. Які типи Thread Pool існують в Java]]
- [[16. В чому різниця між Executors.newFixedThreadPool() та newCachedThreadPool()]]
- [[17. Що таке ForkJoinPool]]