Рубрики
Без рубрики

Руководство по java.util.concurrent.Future

Руководство по java.util.concurrent.Future с обзором его нескольких реализаций

Автор оригинала: baeldung.

1. Обзор

В этой статье мы собираемся узнать о Будущие . Интерфейс, который был вокруг с Java 1.5 и может быть весьма полезным при работе с асинхронными вызовами и параллельной обработки.

2. Создание фьючерсов

Проще говоря, Будущие класс представляет собой будущий результат асинхронных вычислений – результат, который в конечном итоге появится в Будущие после завершения обработки.

Давайте посмотрим, как написать методы, которые создают и возвращают Будущие пример.

Долгосрочные методы являются хорошими кандидатами для асинхронной обработки и Будущие интерфейс. Это позволяет нам выполнять некоторые другие процессы в ожидании задачи, инкапсулированной в Будущие для завершения.

Некоторые примеры операций, которые будут использовать async характер Будущие ар:

  • вычислительные интенсивные процессы (математические и научные расчеты)
  • манипулирование большими структурами данных (большие данные)
  • удаленные вызовы метода (загрузка файлов, HTML слом, веб-сервисы).

2.1. Реализация фьючерсов с FutureTask

Например, мы создадим очень простой класс, который вычисляет квадрат Интегер . Это определенно не соответствует категории “долгосрочных” методов, но мы собираемся поставить Thread.sleep () вызов к нему, чтобы сделать его последние 1 секунду, чтобы завершить:

public class SquareCalculator {    
    
    private ExecutorService executor 
      = Executors.newSingleThreadExecutor();
    
    public Future calculate(Integer input) {        
        return executor.submit(() -> {
            Thread.sleep(1000);
            return input * input;
        });
    }
}

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

Это становится все более интересным, когда мы будем направлять наше внимание на использование Вызов и ИсполнительСервис .

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

Создание экземпляра Вызов не принимает нас в любом месте, мы все еще должны передать этот экземпляр исполнителю, который будет заботиться о запуске этой задачи в новом потоке и вернуть нам ценную Будущие объект. Вот где ИсполнительСервис приходит.

Есть несколько способов, которыми мы можем получить ahold ИсполнительСервис например, большинство из них предоставляются утилитой класса Исполнители статические методы завода. В этом примере мы использовали основную newSingleThreadExecutor() , что дает нам ИсполнительСервис способны обрабатывать один поток за один раз.

Как только у нас будет ИсполнительСервис объект, мы просто должны позвонить представить () проходя мимо нашего Вызов в качестве аргумента. представить () будет заботиться о запуске задачи и вернуть FutureTask объект, который является реализацией Будущие интерфейс.

3. Потребление фьючерсов

До этого момента мы научились создавать экземпляр Будущие .

В этом разделе мы узнаем, как работать с этим экземпляром, исследуя все методы, которые являются частью Будущие API.

3.1. Использование isDone () и получить () для получения результатов

Теперь нам нужно позвонить рассчитать () и использовать возвращенный Будущие чтобы получить полученную Интегер . Два метода из Будущие API поможет нам в этом.

Future.isDone () говорит нам, если исполнитель закончил обработку задачи. Если задача будет выполнена, она вернется истинное в противном случае, он ложные .

Метод, который возвращает фактический результат от расчета, Future.get () . Обратите внимание, что этот метод блокирует выполнение до завершения задачи, но в нашем примере это не будет проблемой, так как мы сначала проверим, завершена ли задача, позвонив isDone () .

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

Future future = new SquareCalculator().calculate(10);

while(!future.isDone()) {
    System.out.println("Calculating...");
    Thread.sleep(300);
}

Integer result = future.get();

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

Метод получить () будет блокировать выполнение до тех пор, пока задача не будет завершена. Но мы не должны беспокоиться об этом, так как наш пример только добраться до точки, где получить () вызывается после того, как убедившись, что задача закончена. Таким образом, в этом сценарии, future.get () всегда будет возвращаться немедленно.

Стоит отметить, что получить () имеет перегруженную версию, которая занимает тайм-аут и ТаймУнит в качестве аргументов:

Integer result = future.get(500, TimeUnit.MILLISECONDS);

Разница между получить (долго, TimeUnit) и получить () , является то, что первый будет бросать Тайм-аутЭксцепция если задача не возвращается до указанного периода тайм-аута.

3.2. Отмена будущего с отменой ()

Предположим, что мы инициировали задачу, но, по некоторым причинам, мы не заботимся о результате больше. Мы можем использовать Future.cancel (булеан) сказать исполнителю, чтобы остановить операцию и прервать ее основной поток:

Future future = new SquareCalculator().calculate(4);

boolean canceled = future.cancel(true);

Наш пример Будущие из вышеуказанного кода никогда не завершит свою работу. В самом деле, если мы попытаемся позвонить получить () из этого случая, после звонка в отменить () , результат будет ОтменаЭксцепция . Future.isCancelled () скажет нам, если Будущие уже отменен. Это может быть весьма полезно, чтобы избежать ОтменаЭксцепция .

Не исключено, что звонок в отменить () Не. В этом случае его возвращенная стоимость будет ложные . Обратите внимание, отменить () занимает булеан значение в качестве аргумента – это контролирует, следует ли прерывать выполнение этой задачи потоком или нет.

4. Больше многопрочитаний с бассейнами резьбы

Наши текущие ИсполнительСервис является одной резьбой, так как она была получена с Исполнители.newSingleThreadExecutor . Чтобы подчеркнуть эту «единую нить», давайте вызвать два расчета одновременно:

SquareCalculator squareCalculator = new SquareCalculator();

Future future1 = squareCalculator.calculate(10);
Future future2 = squareCalculator.calculate(100);

while (!(future1.isDone() && future2.isDone())) {
    System.out.println(
      String.format(
        "future1 is %s and future2 is %s", 
        future1.isDone() ? "done" : "not done", 
        future2.isDone() ? "done" : "not done"
      )
    );
    Thread.sleep(300);
}

Integer result1 = future1.get();
Integer result2 = future2.get();

System.out.println(result1 + " and " + result2);

squareCalculator.shutdown();

Теперь давайте проанализируем выход для этого кода:

calculating square for: 10
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
calculating square for: 100
future1 is done and future2 is not done
future1 is done and future2 is not done
future1 is done and future2 is not done
100 and 10000

Ясно, что этот процесс не является параллельным. Обратите внимание, как вторая задача начинается только после завершения первой задачи, в результате что весь процесс занимает около 2 секунд, чтобы закончить.

Чтобы сделать нашу программу действительно много резьбовой мы должны использовать другой вкус ИсполнительСервис . Давайте посмотрим, как изменится поведение нашего примера, если мы используем пул потоков, предоставляемый заводским методом Исполнители.newFixedThreadPool () :

public class SquareCalculator {
 
    private ExecutorService executor = Executors.newFixedThreadPool(2);
    
    //...
}

С простым изменением в нашей КвадратныйКальцулятор класс теперь у нас есть исполнитель, который может использовать 2 одновременных потоков.

Если мы снова забудем точно такой же клиентский код, мы получим следующий вывод:

calculating square for: 10
calculating square for: 100
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
100 and 10000

Это выглядит гораздо лучше сейчас. Обратите внимание, как 2 задачи начинаются и заканчиваются одновременно, и весь процесс занимает около 1 секунды.

Существуют и другие заводские методы, которые могут быть использованы для создания пулов потоков, такие как Исполнители.newCachedThreadPool () повторное использование ранее использованных Нить s, когда они доступны, и Исполнители.newScheduledThreadPool () который запланиывает команды для запуска после данной задержки .

Для получения дополнительной информации о ИсполнительСервис , читать наши статья посвященный этому вопросу.

5. Обзор ForkJoinTask

ForkJoinTask это абстрактный класс, который реализует Будущие и способен выполнять большое количество задач, выполняемых небольшим числом фактических потоков в ForkJoinPool .

В этом разделе мы собираемся быстро охватить основные характеристики ForkJoinPool . Для всестороннего руководства по этой теме, проверьте наши Руководство по рамочной программе Fork/Join в Java .

Тогда главная характеристика ForkJoinTask является то, что он обычно будет порождать новые подзадаки в рамках работы, необходимой для выполнения своей основной задачи. Он генерирует новые задачи, вызывая вилка () и он собирает все результаты с присоединиться () , таким образом имя типа.

Есть два абстрактных класса, которые реализуют ForkJoinTask : РекурсивНая который возвращает значение по завершении, и Рекурсивнаядействие который ничего не возвращает. Как следует из названий, эти классы должны использоваться для рекурсивных задач, таких как, например, навигация файловой системы или сложные математические вычисления.

Давайте расширим наш предыдущий пример, чтобы создать класс, который, учитывая Интегер , будет рассчитывать сумму квадратов для всех его факторных элементов. Так, например, если мы переведем номер 4 в наш калькулятор, мы должны получить результат от суммы 42 и 32 22 и 12, которая составляет 30.

Прежде всего, нам необходимо создать конкретную реализацию РекурсивНая и реализовать свою вычислить () метод. Здесь мы напишем нашу бизнес-логику:

public class FactorialSquareCalculator extends RecursiveTask {
 
    private Integer n;

    public FactorialSquareCalculator(Integer n) {
        this.n = n;
    }

    @Override
    protected Integer compute() {
        if (n <= 1) {
            return n;
        }

        FactorialSquareCalculator calculator 
          = new FactorialSquareCalculator(n - 1);

        calculator.fork();

        return n * n + calculator.join();
    }
}

Обратите внимание, как мы достигаем рекурсивности, создавая новый экземпляр Факториальный КвадратныйCalculator в вычислить () . Позвонив вилка () , не блокирующий метод, мы просим ForkJoinPool инициировать выполнение этой подзарядка.

присоединиться () метод вернет результат из этого расчета, к которому мы добавим квадрат числа, которое мы в настоящее время посещаем.

Теперь нам просто нужно создать ForkJoinPool для обработки выполнения и управления потоками:

ForkJoinPool forkJoinPool = new ForkJoinPool();

FactorialSquareCalculator calculator = new FactorialSquareCalculator(10);

forkJoinPool.execute(calculator);

6. Заключение

В этой статье мы имели полное представление о Будущие интерфейс, посетив все его методы. Мы также научились использовать возможности пулов потоков для запуска нескольких параллельных операций. Основные методы из ForkJoinTask класс, вилка () и присоединиться () были кратко охвачены, а также.

У нас есть много других отличных статей о параллельных и асинхронных операциях на Java. Вот три из них, которые тесно связаны с Будущие интерфейс (некоторые из них уже упоминаются в статье):

  • Руководство по CompletableFuture – реализация Будущие со многими дополнительными функциями, представленными в Java 8
  • Руководство по рамочной программе Fork/Join в Java – больше о ForkJoinTask мы рассмотрели в разделе 5
  • Руководство по java ExecutorService – посвящено ИсполнительСервис интерфейс

Проверьте исходный код, используемый в этой статье в нашем Репозиторий GitHub .