Що таке Callable та Future?
Callable та Future — це два інтерфейси з java.util.concurrent, які вирішують фундаментальні обмеження Runnable. Якщо Runnable описує задачу без результату (void run()), то Calla...
Callable та Future — це два інтерфейси з java.util.concurrent, які вирішують фундаментальні обмеження Runnable. Якщо Runnable описує задачу без результату (void run()), то Callable — задачу з результатом (V call()), а Future — “обіцянку” цього результату, яку можна отримати пізніше.
Чому це потрібно: у реальному додатку більшість задач повертають результат (відповідь від БД, HTTP-відповідь, результат обчислення). Runnable не може ні повернути значення, ні кинути checked exception — це робить його непридатним для багатьох сценаріїв.
Junior рівень
Базове розуміння
Callable та Future — це інтерфейси з пакету java.util.concurrent, які вирішують обмеження Runnable:
| Проблема Runnable | Рішення |
|---|---|
| Не може повернути результат | Callable — повертає результат типу V |
| Не може кинути checked exception | Callable — метод call() throws Exception |
| Не дізнатися статус задачі | Future — статус, результат, скасування |
Callable — це як Runnable, але з результатом. Ви визначаєте задачу, яка щось обчислює і повертає. Future — це “розписка” від ExecutorService: ви віддали Callable на виконання і отримали Future, за яким пізніше можете забрати результат.
Callable
// Callable — як Runnable, але повертає результат
Callable<String> task = () -> {
// Довга операція
return "Результат!";
};
// Runnable — не повертає
Runnable runnable = () -> {
System.out.println("Без результату");
};
Future
ExecutorService executor = Executors.newFixedThreadPool(2);
// submit повертає Future
Future<String> future = executor.submit(() -> {
Thread.sleep(2000);
return "Готово!";
});
// Перевіряємо статус
System.out.println("Задача завершена? " + future.isDone()); // false
// Отримуємо результат (блокує до завершення!)
String result = future.get(); // Чекаємо 2 секунди...
System.out.println(result); // "Готово!"
System.out.println("Задача завершена? " + future.isDone()); // true
Простий приклад
Callable<Integer> calculateTask = () -> {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
};
Future<Integer> future = executor.submit(calculateTask);
Integer result = future.get(); // Блокує до завершення
System.out.println("Сума: " + result); // 5050
Middle рівень
Callable vs Runnable
| Характеристика | Runnable | Callable |
|---|---|---|
| Метод | void run() |
V call() throws Exception |
| Повертає результат | Ні | Так |
| Checked Exception | Ні | Так |
| Функціональний інтерфейс | Так | Так |
// Runnable:
Runnable r = () -> System.out.println("Hello");
// Callable:
Callable<String> c = () -> {
if (error) throw new IOException("Помилка!"); // Checked!
return "Success";
};
Future — методи
| Метод | Опис |
|---|---|
get() |
Отримати результат (блокує) |
get(timeout, unit) |
Отримати з таймаутом |
isDone() |
Задача завершена? |
isCancelled() |
Задача скасована? |
cancel(mayInterrupt) |
Скасувати задачу |
Future з таймаутом
Future<String> future = executor.submit(() -> {
Thread.sleep(10000); // Довга задача
return "Result";
});
try {
String result = future.get(3, TimeUnit.SECONDS);
System.out.println(result);
} catch (TimeoutException e) {
future.cancel(true); // Скасовуємо задачу
System.out.println("Задача не встигла завершитися");
}
invokeAll() — чекати ВСІ задачі
List<Callable<String>> tasks = List.of(
() -> "Task 1",
() -> "Task 2",
() -> "Task 3"
);
// Блокує, поки ВСІ задачі не завершаться
List<Future<String>> futures = executor.invokeAll(tasks);
for (Future<String> f : futures) {
System.out.println(f.get()); // Task 1, Task 2, Task 3
}
invokeAny() — чекати ПЕРШУ задачу
List<Callable<String>> tasks = List.of(
() -> { Thread.sleep(3000); return "Slow"; },
() -> { Thread.sleep(1000); return "Fast"; },
() -> { Thread.sleep(2000); return "Medium"; }
);
// Повертає результат ПЕРШОЇ завершеної задачі
// Решта автоматично скасовуються
String result = executor.invokeAny(tasks); // "Fast"
Скасування задач
Future<?> future = executor.submit(() -> {
for (int i = 0; i < 1_000_000; i++) {
// Важливо: перевіряти переривання!
if (Thread.currentThread().isInterrupted()) {
return; // Задача скасована
}
process(i);
}
});
// Скасування
boolean cancelled = future.cancel(true);
// true = послати interrupt() потоку
// Але це НЕ гарантує зупинку — задача має перевіряти isInterrupted()!
Senior рівень
Under the Hood: FutureTask
Коли ви викликаєте executor.submit(callable), екзекутор загортає ваш Callable у внутрішній об'єкт FutureTask`:
// Спрощено
public class FutureTask<V> implements RunnableFuture<V> {
private volatile int state; // NEW, COMPLETING, NORMAL, EXCEPTIONAL, CANCELLED
private Callable<V> callable;
private Object outcome; // Результат або виняток
public V get() throws InterruptedException, ExecutionException {
if (state < COMPLETING) {
// Блокує через AwaitNode (черга очікування)
awaitDone(false, 0L);
}
return report(state);
}
}
Машина станів FutureTask
| Стан | Опис |
|---|---|
| NEW | Задача створена |
| COMPLETING | Результат записується (перехідний) |
| NORMAL | Успішне завершення |
| EXCEPTIONAL | Викинуто виняток |
| CANCELLED | Задача скасована (до запуску) |
| INTERRUPTING | Задача скасована (перервана) |
Проблема блокуючого get()
// get() БЛОКУЄ поточний потік
Future<String> future = executor.submit(callable);
String result = future.get(); // Потік зайнятий — не може обробляти інші запити
// Якщо у вас 100 потоків і кожен чекає future.get() → система "встала"
Рішення: CompletableFuture (Java 8+):
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return fetchData();
});
future.thenAccept(result -> {
// Callback — НЕ блокує!
System.out.println(result);
});
// Потік вільний — може продовжувати обробляти запити
ExecutionException — unwrap причини
Future<Integer> future = executor.submit(() -> {
throw new IOException("Database connection failed");
});
try {
future.get();
} catch (ExecutionException e) {
// e — ExecutionException
// e.getCause() — IOException: Database connection failed
Throwable cause = e.getCause();
System.out.println(cause.getClass()); // java.io.IOException
System.out.println(cause.getMessage()); // Database connection failed
}
Втрачені результати (Lost Results)
Future<String> future = executor.submit(() -> "Result");
future.cancel(true); // Скасували
try {
future.get(); // → CancellationException
} catch (CancellationException e) {
System.out.println("Задача була скасована");
}
invokeAll() з таймаутом
// ЗАВЖДИ використовуйте таймаут!
List<Future<String>> futures = executor.invokeAll(tasks, 10, TimeUnit.SECONDS);
// Якщо хоча б одна задача "зависла" — TimeoutException для всіх
Memory Overhead
Кожен FutureTask = об'єкт (~48 байт)
Мільйон задач/сек = мільйон об'єктів → GC pressure
Для віртуальних потоків — краще використовувати легші механізми
Діагностика
Без блокування
Future<?> future = executor.submit(task);
// Перевірка без блокування
if (future.isDone()) {
System.out.println("Завершена");
}
if (future.isCancelled()) {
System.out.println("Скасована");
}
ListenableFuture (Guava)
Для старих проєктів без Java 8+:
ListenableFuture<String> future = listeningExecutor.submit(callable);
Futures.addCallback(future, new FutureCallback<String>() {
public void onSuccess(String result) {
// Callback — не блокує
}
public void onFailure(Throwable t) {
// Обробка помилки
}
}, executor);
Best Practices
- Використовуйте Callable — коли потрібен результат або checked exception
- Завжди timeout для get() —
get(5, TimeUnit.SECONDS)замістьget() - CompletableFuture для неблокуючого коду — Java 8+
- invokeAny() — для запитів до дзеркал
- invokeAll() з таймаутом — для пакетного виконання
- Перевіряйте isInterrupted() — всередині довгих Callable для підтримки cancel()
- ExecutionException.getCause() — для отримання реальної причини
- Уникайте мільйона FutureTask — використовуйте CompletableFuture або VT
Коли НЕ використовувати Callable/Future
- Задача без результату (логування, відправка метрик) — використовуйте Runnable, немає сенсу загортати void у Callable
- Прості однопотокові обчислення — якщо не потрібне паралельне виконання, Callable додає зайву складність
- Потрібен неблокуючий callback — Future.get() блокує потік. Використовуйте CompletableFuture для асинхронних callback-ланцюжків
- Java 21+ з Virtual Threads — для I/O-bound задач простіше створити VT і повернути результат через замикання, ніж загортати у Callable/Future
- Дуже багато коротких задач (мільйон/сек) — кожен FutureTask = об’єкт (~48 байт), GC pressure. Використовуйте lock-free структури або batch-обробку
Callable/Future vs CompletableFuture vs Virtual Threads: що обрати?
| Ситуація | Вибір | Чому |
|---|---|---|
| Задача з результатом + blocking get | Callable + Future | Просто, зрозуміло |
| Задача з результатом + таймаут | Callable + Future.get(timeout) | Вбудована підтримка |
| Неблокуючий callback | CompletableFuture | thenAccept, thenApply — без блокування |
| Багато I/O-bound задач (Java 21+) | Virtual Thread | Простий блокуючий код, масштабується |
| Запит до дзеркал (хто швидше) | invokeAny() або ShutdownOnSuccess | Авто скасування повільних |
| Пакетне виконання | invokeAll() з таймаутом | Чекає всі, з обмеженням за часом |
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- Callable
повертає результат (V call()) і може кинути checked exception; Runnable — void run(), без checked exception - Future — “розписка” від Executor: get() (блокує), isDone(), cancel(mayInterrupt)
- Завжди використовуйте таймаут для get():
get(5, TimeUnit.SECONDS)замістьget() - invokeAll() — чекає ВСІ задачі, invokeAny() — результат ПЕРШОЇ завершеної (решту скасовується)
- ExecutionException.getCause() — unwrap реальної причини помилки
- FutureTask стани: NEW → COMPLETING → NORMAL/EXCEPTIONAL/CANCELLED
- CompletableFuture — неблокуюча альтернатива (thenAccept, thenApply)
Часті уточнюючі запитання:
- Чому cancel(true) не гарантує зупинку задачі? — cancel посилає interrupt(), але задача має сама перевіряти isInterrupted()
- Чим CompletableFuture кращий за Future? — Future.get() блокує потік; CompletableFuture надає callback-ланцюжки (thenAccept, thenApply) без блокування
- Який overhead у FutureTask? — ~48 байт на об’єкт; мільйон задач/сек = GC pressure
- **Чому invokeAll() повертає List
, а не List<результатів>?** — Бо кожна задача може завершитися з винятком; результат потрібно отримати через future.get() з обробкою ExecutionExceptionрезультатів>
Червоні прапорці (НЕ говорити):
- ❌ “Future.get() — це асинхронна операція” — get() блокує викликаючий потік до завершення задачі
- ❌ “cancel() миттєво зупиняє задачу” — cancel лише посилає interrupt, задача має його обробити
- ❌ “Callable потрібен для задач без результату” — для задач без результату використовуйте Runnable, Callable — коли потрібен результат або checked exception
- ❌ “Future можна перевикористати” — Future одноразовий; для повторного виконання потрібен новий submit
Пов’язані теми:
- [[26. Що таке structured concurrency]]
- [[27. В чому різниця між Thread та Runnable]]
- [[23. Що таке Virtual Threads в Java 21]]
- [[22. Як уникнути race condition]]