Питання 27 · Розділ 9

Що таке Callable та Future?

Callable та Future — це два інтерфейси з java.util.concurrent, які вирішують фундаментальні обмеження Runnable. Якщо Runnable описує задачу без результату (void run()), то Calla...

Мовні версії: 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 Задача скасована (до запуску)
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

  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]]