Вступление
В этой статье мы рассмотрим функциональные возможности интерфейса
Future как одной из конструкций параллелизма Java. Мы также рассмотрим
несколько способов создания асинхронной задачи, потому что Future
-
это всего лишь способ представить результат асинхронных вычислений.
Пакет java.util.concurrent
был добавлен в Java 5. Этот пакет содержит
набор классов, упрощающих разработку параллельных приложений на Java. В
общем, параллелизм - довольно сложная тема, которая может показаться
немного устрашающей.
Future
Java очень похоже на Promise
JavaScript.
Мотивация
Распространенной задачей для асинхронного кода является обеспечение гибкого пользовательского интерфейса в приложении, выполняющем дорогостоящие вычисления или операцию чтения / записи данных.
Зависание экрана или отсутствие индикации того, что процесс выполняется, приводит к довольно плохому взаимодействию с пользователем. То же самое и с приложениями, которые работают очень медленно:
Минимизация времени простоя путем переключения задач может значительно улучшить производительность приложения, хотя это зависит от того, какие операции задействованы.
Получение веб-ресурса в целом может быть отложенным или медленным. Чтение огромного файла может быть медленным. Ожидание результата каскадирования микросервисов может быть медленным. В синхронных архитектурах приложение, ожидающее результата, ожидает завершения всех этих процессов, прежде чем продолжить.
В асинхронных архитектурах он тем временем продолжает делать то, что может, без возвращаемого результата.
Выполнение
Прежде чем приступить к примерам, давайте рассмотрим основные интерфейсы
и классы из java.util.concurrent
, который мы собираемся использовать.
Интерфейс Java Callable
- это улучшенная версия Runnable
. Он
представляет собой задачу, которая возвращает результат и может вызвать
исключение. Чтобы реализовать Callable
, вы должны реализовать метод
call()
без аргументов.
Чтобы отправить наш Callable
для одновременного выполнения, мы будем
использовать ExecutorService
. Самый простой способ создать
ExecutorService
- использовать один из фабричных методов класса
Executors
После создания асинхронной задачи из исполнителя
возвращается объект Future
Если вы хотите узнать больше о The Executor Framework , у нас есть подробная статья об этом.
Будущий интерфейс
Интерфейс Future
- это интерфейс, который представляет результат,
который в конечном итоге будет возвращен в будущем. Мы можем проверить,
получил ли Future
результат, ожидает ли оно результата или потерпело
неудачу, прежде чем мы попытаемся получить к нему доступ, что мы
рассмотрим в следующих разделах.
Давайте сначала посмотрим на определение интерфейса:
public interface Future<V> {
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()
будет блокировать приложения , если вы
называете это до того , как результат был возвращен.
Вы также можете указать timeout
истечении которого метод get()
выдаст исключение, если результат еще не был возвращен, что предотвратит
появление огромных узких мест.
Метод cancel()
пытается отменить выполнение текущей задачи. Попытка не
удастся, если задача уже выполнена, была отменена или не может быть
отменена по другим причинам.
isDone()
и isCancelled()
предназначены для определения текущего
статуса связанной задачи Callable
Обычно вы используете их как
условные выражения, чтобы проверить, имеет ли смысл использовать методы
get()
или cancel()
Вызываемый интерфейс
Создадим задачу, на выполнение которой нужно время. Мы определим
DataReader
который implements Callable
:
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.SECONDS.sleep()
. Он вызывает 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 секунд.
Выполнение будущих задач
Теперь, чтобы вызвать эти методы из другого, мы DataProcessor
экземпляр исполнителя и отправим ему DataReader
Исполнитель возвращает
Future
, поэтому мы упакуем его результат в объект Future
-wrapped:
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<String> dataReadFuture = executorService.submit(new DataReader());
Future<String> 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());
}
Здесь мы создали исполнителя с двумя потоками в пуле, поскольку у нас
есть две задачи. Вы можете использовать newSingularThreadExecutor()
для создания одного, если у вас есть только одна параллельная задача для
выполнения.
Если мы отправим более этих двух задач в пул this, дополнительные задачи будут ждать в очереди, пока не появится свободное место.
Выполнение этого фрагмента кода даст:
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
Общее время выполнения составит ~ 5 с, а не ~ 10, поскольку оба они
выполнялись одновременно. Как только мы отправили классы исполнителю,
были вызваны call()
Даже наличие у Thread.sleep()
одной секунды пять
раз не сильно влияет на производительность, поскольку он выполняется в
собственном потоке.
Важно отметить, что код не работал быстрее, он просто не ждал избыточно чего-то, что ему не нужно, а тем временем выполнял другие задачи.
Здесь важно использование метода isDone()
. Если бы у нас не было
чека, не было бы никакой гарантии, что результаты были упакованы в
Future
до того, как мы получим к ним доступ. В противном случае
get()
блокировали бы приложение, пока не получили результаты.
Будущее тайм-аут
Если не было проверок выполнения будущих задач:
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<String> dataReadFuture = executorService.submit(new DataReader());
Future<String> 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 секунд каждый. Если другая задача dataReadFuture
время ожидания в секунду, dataReadFuture возвращается в течение
дополнительных 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()
его. Если его не
cancelled
, печатаем значение полученной String. Напротив, мы
уведомляем пользователя, что в противном случае задача была отменена.
Стоит отметить, что метод cancel()
принимает boolean
параметр. Это
boolean
определяет, разрешаем ли мы cancel()
прерывать выполнение
задачи или нет. Если мы установим его как false
, есть вероятность,
что задача не будет отменена.
Мы также должны присвоить возвращаемое значение метода cancel()
boolean
значению. Возвращаемое значение показывает, успешно ли был
выполнен метод. Если не удается отменить задачу, boolean
будет
установлено как 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)
Ограничения будущего
Future
Java было хорошим шагом к асинхронному программированию. Но,
как вы уже могли заметить, это элементарно:
Future
не может быть заполнен явно (установка его значения и статуса).- У него нет механизма для создания связанных друг с другом этапов обработки.
- Не существует механизма для
Future
и последующего объединения их результатов. - В
Future
нет никаких конструкций обработки исключений.
К счастью, Java предоставляет конкретные реализации Future, которые
предоставляют эти функции ( CompletableFuture
, CountedCompleter
,
ForkJoinTask, FutureTask
и т. Д.).
Заключение
Когда вам нужно дождаться завершения другого процесса без блокировки, может быть полезно перейти в асинхронный режим. Такой подход помогает повысить удобство использования и производительность приложений.
Java включает специальные конструкции для параллелизма. Базовым является
Java Future
который представляет результат асинхронных вычислений и
предоставляет базовые методы для обработки процесса.