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

Руководство по будущему интерфейсу на Java

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

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

Вступление

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

Пакет java.util.concurrent был добавлен в Java 5. Этот пакет содержит набор классов, который упрощает разработку параллельных приложений на Java. В общем, параллелизм-довольно сложная тема, и она может показаться немного сложной.

Java Будущее очень похоже на JavaScript Обещание .

Мотивация

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

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

Минимизация времени простоя за счет переключения задач может значительно повысить производительность приложения, хотя это зависит от того, какие операции выполняются.

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

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

Реализация

Прежде чем начать с примеров, давайте рассмотрим основные интерфейсы и классы из пакета java.util.concurrent , который мы собираемся использовать.

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

Чтобы отправить наш Вызываемый для одновременного выполнения, мы будем использовать ExecutorService . Самый простой способ создать ExecutorService – это использовать один из заводских методов класса Executors . После создания асинхронной задачи от исполнителя возвращается объект Java Future .

Если вы хотите узнать больше о структуре исполнителя , у нас есть подробная статья об этом.

Будущий интерфейс

Интерфейс Future – это интерфейс, представляющий результат, который в конечном итоге будет возвращен в будущем. Мы можем проверить, был ли передан результат Future , ожидает ли он результата или потерпел неудачу, прежде чем мы попытаемся получить к нему доступ, о чем мы расскажем в следующих разделах.

Давайте сначала взглянем на определение интерфейса:

public interface Future {
	V get() throws InterruptedException, ExecutionException;
	V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
	boolean isCancelled();
	boolean isDone();
	boolean cancel(boolean mayInterruptIfRunning)
}

Метод get() извлекает результат. Если результат еще не был возвращен в экземпляр Future , метод get () | будет ждать возврата результата. Важно отметить, что get () заблокирует ваше приложение, если вы вызовете его до того, как результат будет возвращен.

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

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

Это сделано() и Отменено() методы предназначены для определения текущего состояния связанной Вызываемой задачи. Обычно вы используете их в качестве условных обозначений, чтобы проверить, имеет ли смысл использовать методы get() или cancel () .

Вызываемый Интерфейс

Давайте создадим задачу, выполнение которой займет некоторое время. Мы определим DataReader , который реализует вызываемый :

public class DataReader implements Callable {
    @Override
    public String call() throws Exception {
        System.out.println("Reading data...");
        TimeUnit.SECONDS.sleep(5);
        return "Data reading finished";
    }
}

Чтобы смоделировать дорогостоящую операцию, мы используем TimeUnit.СЕКУНДЫ.сон() . Он вызывает Thread.sleep() , но немного чище в течение более длительных периодов времени.

Аналогично, давайте создадим класс процессора, который одновременно обрабатывает некоторые другие данные:

public class DataProcessor implements Callable {
    @Override
    public String call() throws Exception {
        System.out.println("Processing data...");
        TimeUnit.SECONDS.sleep(5);
        return "Data is processed";
    }
}

Оба этих метода выполняются по 5 секунд каждый. Если бы мы просто вызывали друг друга синхронно, чтение и обработка заняли бы ~10 секунд.

Выполнение Будущих Задач

Как вызвать эти методы из другого, мы создадим экземпляр исполнителя и отправим ему ваш DataReader и Обработчик данных . Исполнитель возвращает Future , поэтому мы упакуем его результат в Future -обернутый объект:

public static void main(String[] args) throws InterruptedException, ExecutionException {
    ExecutorService executorService = Executors.newFixedThreadPool(2);

    Future dataReadFuture = executorService.submit(new DataReader());
    Future dataProcessFuture = executorService.submit(new DataProcessor());

    while (!dataReadFuture.isDone() && !dataProcessFuture.isDone()) {
            System.out.println("Reading and processing not yet finished.");
            // Do some other things that don't depend on these two processes
            // Simulating another task
            TimeUnit.SECONDS.sleep(1);
        }
    System.out.println(dataReadFuture.get());
    System.out.println(dataProcessFuture.get());
}

Здесь мы создали исполнителя с двумя потоками в пуле, так как у нас есть две задачи. Вы можете использовать new Singular Thread Executor() для создания одного, если у вас есть только одна одновременная задача для выполнения.

Если мы отправим более этих двух задач в этот пул, дополнительные задачи будут ждать в очереди, пока не появится свободное место.

Запуск этого фрагмента кода приведет к:

Reading and processing not yet finished.
Reading data...
Processing data...
Reading and processing not yet finished.
Reading and processing not yet finished.
Reading and processing not yet finished.
Reading and processing not yet finished.
Data reading finished
Data is processed

Git Essentials

Ознакомьтесь с этим практическим руководством по изучению Git, содержащим лучшие практики и принятые в отрасли стандарты. Прекратите гуглить команды Git и на самом деле изучите это!

Общее время выполнения составит ~5 секунд, а не ~10 секунд, так как оба они выполнялись одновременно. Как только мы отправили классы исполнителю, были вызваны их методы call () . Даже имея Thread.sleep() в течение одной секунды пять раз не сильно влияет на производительность, так как он работает в своем собственном потоке.

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

Что здесь важно, так это использование метода isDone () . Если бы у нас не было чека, не было бы никакой гарантии, что результаты были упакованы в Будущем до того, как мы получили к ним доступ. Если бы это было не так, методы get() заблокировали бы приложение до тех пор, пока у них не появятся результаты.

Время ожидания в будущем

Если бы не было проверок на выполнение будущих задач:

public static void main(String[] args) throws InterruptedException, ExecutionException {
    ExecutorService executorService = Executors.newFixedThreadPool(2);

    Future dataReadFuture = executorService.submit(new DataReader());
    Future dataProcessFuture = executorService.submit(new DataProcessor());

    System.out.println("Doing another task in anticipation of the results.");
    // Simulating another task
    TimeUnit.SECONDS.sleep(1);
    System.out.println(dataReadFuture.get());
    System.out.println(dataProcessFuture.get());
}

Время выполнения все равно составит ~5 секунд, хотя мы столкнемся с большой проблемой. Для выполнения дополнительной задачи требуется 1 секунда, а для выполнения двух других-5 секунд.

Звучит так же, как в прошлый раз?

4 из 5 секунд в этой программе блокируются. Мы попытались получить результат будущего до того, как он был возвращен, и заблокировали 4 секунды, пока они не вернутся.

Давайте установим ограничение для получения этих методов. Если они не вернутся в течение определенного ожидаемого периода времени, они будут создавать исключения:

String dataReadResult = null;
String dataProcessResult = null;

try {
    dataReadResult = dataReadFuture.get(4, TimeUnit.SECONDS);
    dataProcessResult = dataProcessFuture.get(0, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
    e.printStackTrace();
}

System.out.println(dataReadResult);
System.out.println(dataProcessResult);

Оба они занимают по 5 секунд каждый. С опережением на секунду от другой задачи функция чтения данных возвращается в течение дополнительных 4 секунд. Результат обработки данных возвращается одновременно, и этот код работает хорошо.

Если бы мы дали ему нереальное время для выполнения (в общей сложности менее 5 секунд), нас бы встретили:

Reading data...
Doing another task in anticipation of the results.
Processing data...
java.util.concurrent.TimeoutException
	at java.util.concurrent.FutureTask.get(FutureTask.java:205)
	at FutureTutorial.Main.main(Main.java:21)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
null
null

Конечно, мы бы не просто распечатали трассировку стека в реальном приложении, а скорее перенаправили логику для обработки исключительного состояния.

Функции Отмены

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

Таким образом, вы освобождаете место для другой задачи в очереди или просто высвобождаете ресурсы, выделенные для ненужной дорогостоящей операции:

boolean cancelled = false;
if (dataReadFuture.isDone()) {
    try {
        dataReadResult = dataReadFuture.get();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
} else {
cancelled = dataReadFuture.cancel(true);
}
if (!cancelled) {
    System.out.println(dataReadResult);
} else {
    System.out.println("Task was cancelled.");
}

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

Что стоит отметить, так это то, что метод cancel() принимает логический параметр. Это логическое определяет, разрешаем ли мы методу cancel() прерывать выполнение задачи или нет. Если мы установим его как false , есть вероятность, что задача не будет отменена.

Мы также должны присвоить возвращаемое значение метода cancel() логическому . Возвращаемое значение указывает, успешно ли запущен метод или нет. Если не удается отменить задачу, логическое значение будет установлено как false .

Выполнение этого кода приведет к:

Reading data...
Processing data...
Task was cancelled.

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

if (dataReadFuture.cancel(true)) {
    dataReadFuture.get();
}

Выполнение этого кода приведет к:

Processing data...
Exception in thread "main" java.util.concurrent.CancellationException
	at java.util.concurrent.FutureTask.report(FutureTask.java:121)
	at java.util.concurrent.FutureTask.get(FutureTask.java:192)
	at FutureTutorial.Main.main(Main.java:34)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

Ограничения будущего

Java Будущее было хорошим шагом на пути к асинхронному программированию. Но, как вы уже, возможно, заметили, это элементарно:

  • Будущие s не могут быть явно завершены (установка его значения и статуса).
  • В нем нет механизма для создания взаимосвязанных этапов обработки.
  • Нет механизма для параллельного запуска Future s и последующего объединения их результатов вместе.
  • В Future нет никаких конструкций обработки исключений.

К счастью, Java предоставляет конкретные будущие реализации, которые предоставляют эти функции ( CompletableFuture , CountedCompleter , ForkJoinTask, FutureTask и т. Д.).

Вывод

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

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