Java Callable и Future
Возвращайте значения из задач с Callable и получайте их асинхронно через Future — ожидание, тайм-аут, отмена, обработка исключений.
Runnable позволяет потоку выполнять работу. Но он не позволяет вернуть значение или выбросить проверяемое исключение. Для этого предназначена пара: Callable<V> (производитель) и Future<V> (потребитель). Вы передаёте Callable<V> в ExecutorService и получаете обратно Future<V> — дескриптор, с помощью которого можно: ждать результата, считать значение, перехватить исключение задачи или отменить её.
Это самый низкоуровневый API для работы с результатами в конкурентном инструментарии Java. В следующей главе, CompletableFuture, добавляются цепочки, комбинаторы и конвейеры; но контракт — «асинхронный результат, которого можно ждать» — впервые определил именно Future, и он по-прежнему остаётся правильным инструментом для простого «выполни это и сообщи, когда закончишь».
Callable<V> — Runnable с возвращаемым типом
Интерфейс:
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}Два отличия от Runnable:
- Возвращает
V(параметр типа). - Может выбрасывать любое
Exception— в том числе проверяемые исключения.
Как и Runnable, это функциональный интерфейс — работают лямбды и ссылки на методы:
Callable<Integer> compute = () -> {
Thread.sleep(100);
return 42;
};
Callable<String> read = () -> Files.readString(Path.of("config.txt")); // can throw IOException
Callable<List<Order>> query = () -> repo.findAll(); // can throw SQLExceptionCallable подходит для любой задачи вида «выполни это и верни значение». Runnable уместен только тогда, когда результат вам действительно не нужен.
Future<V> — дескриптор асинхронного результата
Когда вы вызываете submit с Callable<V>, исполнитель возвращает Future<V>:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}Пять методов. Три из них используются чаще всего.
get()
Блокирует вызывающий поток до завершения задачи, затем возвращает результат:
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> f = pool.submit(() -> { Thread.sleep(100); return 42; });
Integer value = f.get(); // blocks until done; returns 42get() может выбросить три исключения, которые необходимо обработать:
InterruptedException— вызывающий поток был прерван во время ожидания. Стандартная обработка: восстановить флаг прерывания и передать исключение дальше.ExecutionException— задача сама выбросила исключение. Оригинальное исключение обёрнуто; получить его можно через.getCause().CancellationException— кто-то вызвалcancel()на этом future.
Типичный шаблон:
try {
Integer v = f.get();
} catch (ExecutionException e) {
Throwable cause = e.getCause(); // the real exception the task threw
// ... handle cause ...
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// ... bail out cooperatively ...
}get(timeout, unit)
То же, что get(), но с дедлайном. Выбрасывает TimeoutException, если задача не завершилась вовремя:
try {
Integer v = f.get(500, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
f.cancel(true); // give up; ask the task to stop
throw new ServiceUnavailableException("timed out");
}Это правильный шаблон для «я обращаюсь к бэкенду, который должен ответить за N мс; иначе — быстрый сбой». Всегда сопровождайте перехват cancel(true) — иначе задача продолжит выполняться в фоне, занимая поток, результат которого вам уже не нужен.
cancel(boolean)
Просит задачу остановиться:
boolean cancelled = f.cancel(true); // true = interrupt the running threadАргумент указывает исполнителю, нужно ли прерывать рабочий поток. При true рабочий поток получит InterruptedException от любого блокирующего вызова (sleep, wait, I/O); при false отмена не имеет эффекта, если задача уже запущена — из очереди удаляются только ещё не стартовавшие задачи.
cancel работает кооперативно. Задача, которая не проверяет Thread.currentThread().isInterrupted() и не делает блокирующих вызовов, продолжит выполняться до конца. Отмена — не аварийное завершение, а запрос, который задача должна соблюдать.
Исключения: правило обёртки
Всё, что выбросит Callable, при вызове get оборачивается в ExecutionException. Причина — исходный throwable:
Future<Integer> f = pool.submit(() -> { throw new IOException("nope"); });
try {
f.get();
} catch (ExecutionException e) {
e.getCause(); // IOException("nope")
e.getCause() instanceof IOException; // true
}Обратите внимание на асимметрию: Callable может выбросить проверяемое исключение (объявленное throws Exception в сигнатуре), но Future.get объявляет только ExecutionException. Обёртка позволяет одной сигнатуре нести любой возможный сбой.
Перегрузка Runnable.submit — pool.submit(Runnable) — возвращает Future<?>, чей get() при успехе возвращает null и по-прежнему оборачивает любое непойманное RuntimeException из Runnable. Это стандартный способ обнаружить, что «запустили и забыли» Runnable на самом деле завершился с ошибкой.
Ограничения Future
Future — однонаправленный канал: вы отправляете задачу, ждёте и получаете значение. Он не поддерживает композицию:
- Нельзя сказать «когда это завершится — запусти то с результатом».
- Нельзя сказать «когда завершится любой из этих N — сделай X».
- Нельзя сказать «объедини результаты этих двух futures без блокировки».
Для всего этого нужен CompletableFuture (следующая глава). Future — правильный инструмент, когда:
- Нужно просто получить значение от одной задачи.
- Вы используете API, возвращающий
Future, и не нужна композиция. - Простейшего контракта достаточно.
В современном коде с активной асинхронной композицией вы чаще всего будете пропускать Future и сразу использовать CompletableFuture — но Future — это тип, который ExecutorService по-прежнему возвращает из submit, поэтому вы встретите оба.
FutureTask — реализация за submit
Класс, который обеспечивает работу submit. Его можно использовать напрямую:
FutureTask<Integer> task = new FutureTask<>(() -> compute());
new Thread(task).start(); // FutureTask is a Runnable
Integer v = task.get();Большинство кода не создаёт FutureTask напрямую — это делает инфраструктура исполнителей. Но он полезен, когда нужен объект, одновременно являющийся Future и Runnable — например, для планирования на чём-то отличном от ExecutorService.
Практический пример: submit, тайм-аут, передача исключений
Программа ниже отправляет медленную, быструю и падающую задачи; демонстрирует get, get(timeout), распаковку исключений и отмену.
Что нужно вынести из запуска:
- Раздел 1 — простейший шаблон: отправить
Callable, вызватьget, получить значение.getблокировал главный поток на 50 мс, которые заняла задача. Именно это и делаетFutureв базовой форме — типизированный, блокирующий дескриптор результата, который придёт позже. - Раздел 2 показал шаблон с тайм-аутом. Медленная задача работала бы 500 мс;
get(100, MS)сдался через 100 и выбросилTimeoutException. Следующийcancel(true)прервал выполняющийся поток, чтобы тот мог завершиться досрочно. Без отмены задача продолжала бы выполняться ещё 400 мс — занимая поток, результат которого вам уже не нужен. - Раздел 3 показал обёртку исключений.
CallableвыбросилIOException;get()перевыбросил его внутриExecutionException.e.getCause()вернул оригинал. Это универсальный канал сбоев API — любой проверяемый или непроверяемый выброс из тела попадает сюда. - Раздел 4 показал отмену незапущенной задачи. Пока оба потока пула заняты
hog1иhog2, задачаqueuedнаходилась в очереди;cancel(false)удалила её, так и не запустив. Вызовget()на отменённом future выбросилCancellationException— это отличается от «задача выбросила исключение» (тогда было быExecutionException). - Раздел 5 показал
invokeAny. Самая быстрая задача (50 мс) победила; остальные две были отменены исполнителем.invokeAny— правильный инструмент для избыточных запросов: обратитесь к нескольким источникам, используйте первый успешный, откажитесь от остальных. Это строительный блок паттерна hedged-request в реальных системах.
Что дальше
Следующая глава, Java CompletableFuture, знакомит с компонуемым асинхронным API — thenApply, thenCompose, allOf, anyOf и десятками комбинаторов, которые превращают Future из однорезультатного дескриптора в полноценный реактивный конвейер.