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

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

Вступление

В этой статье мы рассмотрим функциональные возможности интерфейса 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 который представляет результат асинхронных вычислений и предоставляет базовые методы для обработки процесса.

comments powered by Disqus