Что такое Callable и Future?
Callable и Future — это два интерфейса из java.util.concurrent, которые решают фундаментальные ограничения Runnable. Если Runnable описывает задачу без результата (void run()),...
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 | Задача отменена (до запуска) |
| INTERRUPTED | Задача отменена (прервана) |
Проблема блокирующего 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]]