Асинхронные задачи FutureTask. Callable и Future

Последнее обновление: 18.10.2025

Метод run() интрефейcа Runnable, который выполняется потоком, не возвращает никакого результата и просто выполняет некоторые действия. Однако иногда в потоке необходимо выполнять действия, которые производят некоторый результат. И после завершения необходимо получить из потока этот результат и использовать его каким-то образом. Для этой задачи язык Java предоставляет интерфейсы Callable и Future, которые определены в пакете java.util.concurrent.

Callable представляет некоторое действие, которое возвращает некоторый результат. Этот интерфейс имеет параметр типа и один метод - call(), который возвращает значение этого типа:

public interface Callable<V>{

   V call() throws Exception;
}

То есть, грубо говоря, мы запускаем в потоке объект этого интерфейса, а результат метода call() - это и будет то значение, которое мы получим из потока.

Интерфейс Future представляет результат асинхронной задачи. Используя объект Future, можно получить результат, например, в результате выполнения метода call() интерфейса Callable. Причем Future позволяет получить результат именно тогда, когда он будет готов.

Этот интерфейс предоставляет функционал для проверки завершения вычисления, ожидания его завершения и получения результата:

  • boolean cancel(boolean mayInterruptIfRunning)

    Пытается отменить выполнение задачи

  • default Throwable exceptionNow()

    Возвращает исключение, сгенерированное задачей, без ожидания

  • V get()

    При необходимости ожидает завершения задачи, а затем извлекает его результат

  • V get(long timeout, TimeUnit unit)

    При необходимости ожидает завершения задачи не более заданного времени, а затем извлекает ее результат, если он доступен

  • boolean isCancelled()

    Возвращает true, если задача была отменена до ее нормального завершения

  • boolean isDone()

    Возвращает true, если задача завершена

  • default V resultNow()

    Возвращает вычисленный результат без ожидания, неблокирующий вариант метода get()

  • default Future.State state()

    Возвращает состояние задачи. Представляет перечисление Future.State со следующими константами:

    • CANCELLED: задача отменена.

    • FAILED: задача завершилась неудачно, сгенерировав исключение.

    • RUNNING: задача выполняется и еще не завершена.

    • SUCCESS: задача успешно завершена.

FutureTask

Один из способов запуска асинхронных задач в виде Callable и Future представляет класс FutureTask. Этот класс представляет асинхронную задачу, которую можно отменить. Этот класс предоставляет базовую встроенную реализацию интерфейса Future с методами для запуска и отмены вычисления, запроса на завершение вычисления и получения результата вычисления.

Кроме того, поскольку FutureTask реализует Runnable, то объекты FutureTask могут быть использованы для создания потоков.

Обычно FutureTask применяется для обертки объекта Callable или Runnable для запуска задачи, которая должна быть выполнена в будущем. Для этого класс предоставляет пару конструкторов:

  • FutureTask(Runnable runnable, V result)

    Создает FutureTask, который при запуске выполнит указанный Runnable и обеспечит возврат указанного результата методом get при успешном завершении.

  • FutureTask(Callable<V> callable)

    Создает FutureTask, который при запуске выполнит указанный Callable.

Функциональность класса FutureTask реализуется следующими методами:

  • boolean cancel(boolean mayInterruptIfRunning)

    Пытается отменить выполнение задачи

  • Throwable exceptionNow()

    Возвращает исключение, сгенерированное задачей

  • V get()

    При необходимости ожидает завершения задачи и возвращает результат задачи

  • V get(long timeout, TimeUnit unit)

    При необходимости ожидает завершения задачи не более заданного времени, а затем извлекает его результат, если он доступен

  • boolean isCancelled()

    Возвращает true, если задача была отменена до ее нормального завершения

  • boolean isDone()

    Возвращает true, если задача завершена

  • V resultNow()

    Возвращает результат задачи без ожидания ее завершения

  • void run()

    Устанавливает используемый объект Future в качестве результата задачи, если только она не была отменена

  • Future.State state()

    Возвращает состояние задачи

Ключевым является метод get(), который блокирует выполнение, если задача еще не завершена. И после завершения возвращает ее результат. Посмотрим на примере:

import java.util.concurrent.*;

class Program{

    public static void main(String[] args) {
         
        System.out.println("Main thread started...");

        int number = 5;  // исходное число для вычисления факториала

        // определяем задачу, которая вычисляет факториал
        Callable<Integer> task = () -> {

            int n = number;     // копируем число, чтобы не изменять его
            int result = 1;
            while(n > 0) result *= n--;
            return result;  // возвращаем результат задачи
        };

        // определяем задачу, которая будет выполнена в будущем
        var futureTask = new FutureTask<Integer>(task);
        // создаем и запускаем поток
        var t = new Thread(futureTask); // futureTask - это объект Runnable
        t.start();

        // здесь могут быть какие-либо другие операции метода main
        // которые должны выполняться одновременно с futureTask
        System.out.println("Main thread works...");

        try{
            // ожидаем выполнения задачи
            Integer factorial = futureTask.get();
            System.out.printf("factorial of %d is %d\n", number, factorial);
        }
        catch(Exception ex){ 
            System.out.println(ex.getMessage()); 
        }

        System.out.println("Main thread finished...");
    }
}

Консольный вывод программы:

Main thread started...
Main thread works...
factorial of 5 is 120
Main thread finished...

Разберем эту программу. Прежде всего определяем задачу Callable, которая будет выполняться, в виде лямбда-выражения (мы можем так сделать, потому что Callable имеет только один метод):

Callable<Integer> task = () -> {

    int n = number;     // копируем число, чтобы не изменять его
    int result = 1;
    while(n > 0) result *= n--;
    return result;  // возвращаем результат задачи
};

Здесь простейший код выполнения факториала для числа number, которое определено во внешнем коде (лямбда-выражения могут обращаться в нешним переменным). В конце лямбда-выражение возвращает результат - значение переменной result.

Далее определяем задачу, которая будет выполнена в будущем - объект FutureTask, передавая в его конструктор выше определенную задачу Callable:

var futureTask = new FutureTask<Integer>(task);

Поскольку задача возвращает значение типа int, преобразуемое в Integer, объект FutureTask (как и Callable) типизируется типом Integer

Далее для выполнения задачи создаем и запускаем поток:

var t = new Thread(futureTask); // futureTask - это объект Runnable
t.start();

Поскольку объект FutureTask реализует интерфейс Runnable, то мы можем передать его в конструктор класса Thread.

Пока дочерний поток t выполняет свою задачу - вычисляет факториала, основной поток также может выполнять некоторые действия. В данном случае для примера просто выводим сообщение на консоль:

System.out.println("Main thread works...");

Затем для получения результата задачи у FutureTask вызываем метод get()

try{
    // ожидаем выполнения задачи
    Integer factorial = futureTask.get();
    System.out.printf("factorial of %d is %d\n", number, factorial);
}
catch(Exception ex){ 
    System.out.println(ex.getMessage()); 
}

Этот метод генерирует ряд исключений, поэтому оборачиваем его вызов в конструкцию try..cath. В итоге с помощью метода get() мы получим результат и выведем его на консоль.

Стоит отметить, что при вызове метода get()

Integer factorial = futureTask.get();

Главный поток - метод main приостанавливает выполнение и ждет получения результата (либо пока не произовдет ошибка)

Генерация и обработка ошибки

Но выше определенный способ вычисления факториала имеет большой недостаток - если переданное число будет меньше 1, то задача возвратит число 1, что будет неправильно: для вычисления факториала число должно быть больше 0. И мы можем предупредить эту ситуацию, сгенерировав ошибку. И стоит отметить, что при метод call() интерфейса Callable уже в своем определение предусматривает возможную генерацию ошибки:

V call() throws Exception;

И также стоит отметить, что если FutureTask генерирует исключение, то оно имеет тип ExecutionException. Перехватывая исключения этого типа, мы можем понять, что исключение произошло в методе call при выполнении FutureTask.

Например, сгенерируем ошибку, если входное значение для факториала меньше 1:

import java.util.concurrent.*;

class Program{

    public static void main(String[] args) {
         
        System.out.println("Main thread started...");

        int number = -5;  // исходное число для вычисления факториала
        try{
            // определяем задачу, которая вычисляет факториал
            Callable<Integer> task = () -> {

                // если число меньше 1, генерируем исключение
                if(number < 1) throw new Exception("Number must be greater than 0");
                int n = number;     // копируем число, чтобы не изменять его
                int result = 1;
                while(n > 0) result *= n--;
                return result;  // возвращаем результат задачи
            };

            var futureTask = new FutureTask<Integer>(task);
            var t = new Thread(futureTask);
            t.start();

            System.out.println("Main thread works...");
       
            // ожидаем выполнения задачи
            Integer factorial = futureTask.get();
            System.out.printf("factorial of %d is %d\n", number, factorial);
        }
        // перехватываем ошибки выполнения FutureTask
        catch(ExecutionException ex){

            System.out.println("Factorial error: " + ex.getMessage());
        }
        // перехватываем все остальные ошибки
        catch(Exception ex){ 
            System.out.println(ex.getMessage()); 
        }

        System.out.println("Main thread finished...");
    }
}

Здесь перед вычислением факториала проверяем число и при необходимости генерируем ошибку:

if(number < 1) throw new Exception("Number must be greater than 0");

Для обработки ошибки выполнения во FutureTask определяем специальный блок catch:

catch(ExecutionException ex){

    System.out.println("Factorial error: " + ex.getMessage());
}

И поскольку здесь для вычисления факториала передается заведомо некорректное число - -5, то мы столкнемся с ошибкой:

Main thread started...
Main thread works...
Factorial error: java.lang.Exception: Number must be greater than 0
Main thread finished...

Стоит отметить, что информацию о возникшем исключении мы можем получить и из объекта FutureTask через метод exceptionNow(). Например:

import java.util.concurrent.*;

class Program{

    public static void main(String[] args) {
         
        System.out.println("Main thread started...");

        int number = -5;  // исходное число для вычисления факториала

        FutureTask<Integer> futureTask = null;  // определяем задачу

        try{
            ..............................................

            futureTask = new FutureTask<Integer>(task);

            ..............................................
        }
        // перехватываем ошибки выполнения FutureTask
        catch(ExecutionException ex){

            var ex = futureTask.exceptionNow();  // получаем исключение
            System.out.println("Factorial error: " + ex.getMessage());
        }
        // перехватываем все остальные ошибки
        catch(Exception ex){ 
            System.out.println(ex.getMessage()); 
        }

        System.out.println("Main thread finished...");
    }
}

Это может быть полезно, когда у нас несколько объектов FutureTask, и каждый из которых может сгенерировать исключения, которые надо как-то разграничить.

Отмена задач

Одним из достоинств интерфейса Future является то, что подобную задачу можно отменять. Для этого предназначен метод cancel():

boolean cancel(boolean mayInterruptIfRunning)

Если задача уже запущена, то параметр mayInterruptIfRunning определяет, прерывается ли поток задачи. Возвращаемое значение: false, если задачу нельзя отменить (обычно потому, что она уже завершена), или true, если можно отменить. Но данный результат не обязательно указывает, отменена ли задача - для этой цели у Future используется метод isCancelled().

Этот метод не действует, если задача уже завершена или отменена или не может быть отменена по какой-либо другой причине. В противном случае, если эта задача не запустилась при вызове отмены, эта задача никогда не должна запускаться.

Рассмотрим небольшой пример:

import java.util.concurrent.*;

class Program{

    public static void main(String[] args) {
         
        System.out.println("Main thread started...");

        int number = 5;  // исходное число для вычисления факториала

        try{
            // определяем задачу, которая вычисляет факториал
            Callable<Integer> task = () -> {

                if(number < 1) throw new Exception("Number must be greater than 0");
                int n = number;     // копируем число, чтобы не изменять его
                int result = 1;
                while(n > 0) {

                    result *= n--;
                    System.out.printf("[Factorial execution] result = %d   n = %d\n", result, n);
                    Thread.sleep(200);
                }
                return result;
            };

            var futureTask = new FutureTask<Integer>(task);
            // создаем и запускаем поток
            var t = new Thread(futureTask);
            t.start();

            System.out.println("Main thread works...");
            Thread.sleep(300); // имитируем работу, чтобы futureTask успела немного поработать

            // в конце отменяем задачу
            futureTask.cancel(true);

            // проверяем отмену задачи
            if(futureTask.isCancelled()) System.out.println("Factorial execution cancelled...");
        }
        catch(Exception ex){ 
            System.out.println(ex.getMessage()); 
        }

        System.out.println("Main thread finished...");
    }
}

Здесь для демонстрации в коде задачи Callable делаем небольшую задержку в 200 миллисекунд:

while(n > 0) {

    result *= n--;
    System.out.printf("[Factorial execution] result = %d   n = %d\n", result, n);
    Thread.sleep(200);
}

В основном потоке main останавливаем задачу, причем в прекращением работы потока, и проверяем ее статус остановки:

// отменяем задачу
futureTask.cancel(true);

// проверяем отмену задачи
if(futureTask.isCancelled()) System.out.println("Factorial execution cancelled...");

Консольный вывод программы:

Main thread started...
Main thread works...
[Factorial execution] result = 5   n = 4
[Factorial execution] result = 20   n = 3
Factorial execution cancelled...
Main thread finished...

Здесь мы видим, что после двух итераций цикла в задаче факториала задача была отменена и прекратила свое выполнение.

Помощь сайту
Юмани:
410011174743222
Номер карты:
4048415020898850