Руководство по ExecutorService в Java

Изучите использование Java ExecutorService для выполнения класса Runnable или Callable асинхронным способом. Также изучите различные передовые практики для его наиболее эффективного использования в любом приложении Java.

1. Что такое Executor Framework?

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

Эту проблему решил фреймворк Executor(начиная с Java 1.5). Фреймворк состоит из трех основных интерфейсов(и множества дочерних интерфейсов):

1.1 Преимущества Executor Framework

  • Фреймворк в основном разделяет создание и выполнение задач. Создание задач в основном представляет собой шаблонный код и легко заменяемо.
  • С помощью исполнителя нам необходимо создавать задачи, реализующие интерфейс Runnable или Callable, и отправлять их исполнителю.
  • Executor поддерживает внутренний(настраиваемый) пул потоков для повышения производительности приложения путем избежания постоянного создания потоков.
  • Исполнитель отвечает за выполнение задач и их запуск с использованием необходимых потоков из пула.

1.2. Отзывные и будущие

Другим важным преимуществом фреймворка Executor является использование интерфейса Callable. Он похож на интерфейс Runnable с двумя преимуществами:

  1. Его метод call() возвращает результат после завершения выполнения потока.
  2. Когда мы отправляем объект Callable исполнителю, мы получаем ссылку на объект Future. Мы можем использовать этот объект для запроса статуса потока и результата объекта Callable.

2. Создание экземпляра ExecutorService

ExecutorService — это интерфейс, и его реализации могут выполнять класс Runnable или Callable асинхронным способом. Обратите внимание, что вызов метода run() интерфейса Runnable синхронным способом — это просто вызов метода.

Экземпляр ExecutorService можно создать следующими способами:

2.1 Использование исполнителей

Executors — это служебный класс, предоставляющий фабричные методы для создания реализаций интерфейса.

//Executes only one threadExecutorService es = Executors.newSingleThreadExecutor();//Internally manages thread pool of 2 threadsExecutorService es = Executors.newFixedThreadPool(2);//Internally manages thread pool of 10 threads to run scheduled tasksExecutorService es = Executors.newScheduledThreadPool(10);

2.2 Использование конструкторов

Мы можем выбрать класс реализации интерфейса ExecutorService и создать его экземпляр напрямую. Следующий оператор создает исполнитель пула потоков с минимальным количеством потоков 10, максимальным количеством потоков 100 и временем жизни 5 миллисекунд и очередью блокировки для отслеживания задач в будущем.

ExecutorService executorService = new ThreadPoolExecutor(10, 100, 5L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());

3. Отправка задач в ExecutorService

Обычно задачи создаются путем реализации интерфейса Runnable или Callable. Давайте рассмотрим пример обоих случаев.

3.1 Выполнение готовых к запуску задач

Мы можем выполнять исполняемые файлы, используя следующие методы:

  • void execute(Runnable task) – выполняет указанную команду в определенный момент в будущем.
  • Future submit(Runnable task) – отправляет запускаемую задачу на выполнение и возвращает Future, представляющий эту задачу. Метод get() Future вернет null после успешного завершения.
  • Future submit(Runnable task, T result) – отправляет исполняемую задачу на выполнение и возвращает Future, представляющий эту задачу. Метод get() Future вернет заданный результат после успешного завершения.

В данном примере мы выполняем задачу типа Runnable, используя оба метода.

import java.time.LocalDateTime;import java.util.concurrent.ExecutionException;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Future;import java.util.concurrent.TimeUnit;public class Main{public static void main(String[] args){//Demo taskRunnable runnableTask =() -> {try {TimeUnit.MILLISECONDS.sleep(1000);System.out.println("Current time :: " + LocalDateTime.now());} catch(InterruptedException e) {e.printStackTrace();}};//Executor service instanceExecutorService executor = Executors.newFixedThreadPool(10);//1. execute task using execute() methodexecutor.execute(runnableTask);//2. execute task using submit() methodFuture<String> result = executor.submit(runnableTask, "DONE");while(result.isDone() == false){try{System.out.println("The method return value : " + result.get());break;}catch(InterruptedException | ExecutionException e){e.printStackTrace();}//Sleep for 1 secondtry {Thread.sleep(1000L);} catch(InterruptedException e) {e.printStackTrace();}}//Shut down the executor serviceexecutor.shutdownNow();}}

Вывод программы.

Current time :: 2019-05-21T17:52:53.274Current time :: 2019-05-21T17:52:53.274The method return value : DONE

3.2 Выполнение вызываемых задач

Мы можем выполнять вызываемые задачи, используя следующие методы:

  • Future submit(callableTask) – отправляет задачу, возвращающую значение, на выполнение и возвращает Future, представляющий ожидающие результаты задачи.
  • List<Future> invokeAll(Collection tasks) – выполняет заданные задачи, возвращая список Futures, содержащий их статус и результаты, когда все завершено. Обратите внимание, что результат доступен только тогда, когда все задачи завершены.
    Обратите внимание, что выполненная задача могла быть завершена как нормально, так и с выдачей исключения.
  • List<Future> invokeAll(Collection tasks, timeOut, timeUnit) – выполняет указанные задачи, возвращая список Futures, содержащий их статус и результаты, когда все они будут завершены или истечет время ожидания.

В данном примере мы выполняем задачу типа Callable, используя оба метода.

import java.time.LocalDateTime;import java.util.Arrays;import java.util.List;import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Future;import java.util.concurrent.TimeUnit;public class Main{public static void main(String[] args) throws ExecutionException{//Demo Callable taskCallable<String> callableTask =() -> {TimeUnit.MILLISECONDS.sleep(1000);return "Current time :: " + LocalDateTime.now();};//Executor service instanceExecutorService executor = Executors.newFixedThreadPool(1);List<Callable<String>> tasksList = Arrays.asList(callableTask, callableTask, callableTask);//1. execute tasks list using invokeAll() methodtry{List<Future<String>> results = executor.invokeAll(tasksList);for(Future<String> result : results) {System.out.println(result.get());}}catch(InterruptedException e1){e1.printStackTrace();}//2. execute individual tasks using submit() methodFuture<String> result = executor.submit(callableTask);while(result.isDone() == false){try{System.out.println("The method return value : " + result.get());break;}catch(InterruptedException | ExecutionException e){e.printStackTrace();}//Sleep for 1 secondtry {Thread.sleep(1000L);} catch(InterruptedException e) {e.printStackTrace();}}//Shut down the executor serviceexecutor.shutdownNow();}}

Вывод программы.

Current time :: 2019-05-21T18:35:53.512Current time :: 2019-05-21T18:35:54.513Current time :: 2019-05-21T18:35:55.514The method return value : Current time :: 2019-05-21T18:35:56.515

Обратите внимание, что задачи были завершены с задержкой в ​​1 секунду, поскольку в пуле потоков находится только одна задача. Но когда вы запускаете программу, все первые 3 оператора печати появляются одновременно, поскольку даже если задачи завершены, они ждут завершения других задач в списке.

4. Как завершить работу ExecutorService

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

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

В приведенных выше примерах, если мы закомментируем вызов метода executor.shutdownNow(), то даже после выполнения всех задач основной поток останется активным и JVM не остановится.

Чтобы сообщить службе-исполнителю, что имеющиеся у нее потоки не нужны, нам придется завершить работу службы.

Существует три способа вызова выключения:

  • void shutdown() – инициирует упорядоченное завершение работы, при котором выполняются ранее отправленные задачи, но новые задачи не принимаются.
  • List<Runnable> shutdownNow() – пытается остановить все активно выполняющиеся задачи, останавливает обработку ожидающих задач и возвращает список задач, ожидающих выполнения.
  • void awaitTermination() – блокируется до тех пор, пока не завершится выполнение всех задач после запроса на завершение работы, или не истечет время ожидания, или не будет прерван текущий поток, в зависимости от того, что произойдет раньше.

Используйте любой из трех вышеперечисленных методов с умом в соответствии с требованиями приложения.

5. Лучшие практики

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

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

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

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

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

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

7. Еще примеры

Прокрутить вверх