Вопрос 27 · Раздел 9

Что такое Callable и Future?

Callable и Future — это два интерфейса из java.util.concurrent, которые решают фундаментальные ограничения Runnable. Если Runnable описывает задачу без результата (void run()),...

Версии по языкам: English Russian Ukrainian

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

  1. Используйте Callable — когда нужен результат или checked exception
  2. Всегда timeout для get()get(5, TimeUnit.SECONDS) вместо get()
  3. CompletableFuture для неблокирующего кода — Java 8+
  4. invokeAny() — для запросов к зеркалам
  5. invokeAll() с таймаутом — для пакетного выполнения
  6. Проверяйте isInterrupted() — внутри долгих Callable для поддержки cancel()
  7. ExecutionException.getCause() — для получения реальной причины
  8. Избегайте миллиона 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]]