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

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

1. обзор

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

Дальнейшее чтение:

Руководство по Fork / Join Framework в Java

Введение в инфраструктуру fork / join, представленное в Java 7, и инструменты, помогающие ускорить параллельную обработку, пытаясь использовать все доступные ядра процессора.

Read more

Обзор java.util.concurrent

Откройте для себя содержимое пакета java.util.concurrent.

Read more

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

В этой статье мы рассмотрим различные реализации интерфейса Lock и недавно представленный в Java 9 класс StampedLock.

Read more

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

2.1. Заводские методы классаExecutors

Самый простой способ создатьExecutorService - использовать один из фабричных методов классаExecutors.

Например, следующая строка кода создаст пул потоков с 10 потоками:

ExecutorService executor = Executors.newFixedThreadPool(10);

Есть несколько других фабричных методов для создания предопределенныхExecutorService, соответствующих конкретным вариантам использования. Чтобы найти лучший метод для ваших нужд, обратитесь кOracle’s official documentation.

2.2. Непосредственно создатьExecutorService

ПосколькуExecutorService - это интерфейс, можно использовать экземпляр любой его реализации. В пакетеjava.util.concurrent есть несколько реализаций на выбор, или вы можете создать свою собственную.

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

ExecutorService executorService =
  new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
  new LinkedBlockingQueue());

Вы можете заметить, что приведенный выше код очень похож наsource code заводского методаnewSingleThreadExecutor().. В большинстве случаев подробная ручная настройка не требуется.

3. Назначение задачExecutorService

ExecutorService может выполнять задачиRunnable иCallable. Для простоты в этой статье будут использованы две примитивные задачи. Обратите внимание, что здесь используются лямбда-выражения вместо анонимных внутренних классов:

Runnable runnableTask = () -> {
    try {
        TimeUnit.MILLISECONDS.sleep(300);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};

Callable callableTask = () -> {
    TimeUnit.MILLISECONDS.sleep(300);
    return "Task's execution";
};

List> callableTasks = new ArrayList<>();
callableTasks.add(callableTask);
callableTasks.add(callableTask);
callableTasks.add(callableTask);

Задачи могут быть назначеныExecutorService с помощью нескольких методов, включаяexecute(), который наследуется от интерфейсаExecutor, а такжеsubmit(),invokeAny(), invokeAll().

Методexecute() - этоvoid,, и он не дает возможности получить результат выполнения задачи или проверить статус задачи (запущена она или выполняется).

executorService.execute(runnableTask);

submit() отправляет задачуCallable илиRunnable вExecutorService и возвращает результат типаFuture.

Future future =
  executorService.submit(callableTask);

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

String result = executorService.invokeAny(callableTasks);

invokeAll() назначает набор задачExecutorService,, вызывая выполнение каждой из них, и возвращает результат выполнения всех задач в виде списка объектов типаFuture.

List> futures = executorService.invokeAll(callableTasks);

Теперь, прежде чем идти дальше, необходимо обсудить еще две вещи: выключениеExecutorService и работу с типами возвратаFuture.

4. Завершение работыExecutorService

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

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

С другой стороны, приложение может достичь своего конца, но оно не будет остановлено, потому что ожиданиеExecutorService заставит JVM продолжать работу.

Чтобы правильно завершить работуExecutorService, у нас есть APIshutdown() иshutdownNow().

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

executorService.shutdown();

МетодshutdownNow() пытается немедленно уничтожитьExecutorService, но он не гарантирует, что все запущенные потоки будут остановлены одновременно. Этот метод возвращает список задач, ожидающих обработки. Разработчик должен решить, что делать с этими задачами.

List notExecutedTasks = executorService.shutDownNow();

Хороший способ выключитьExecutorService (который также являетсяrecommended by Oracle) - использовать оба этих метода в сочетании с методомawaitTermination(). При таком подходеExecutorService сначала перестанет принимать новые задачи, ожидая до определенного периода времени для завершения всех задач. Если это время истекает, выполнение немедленно останавливается:

executorService.shutdown();
try {
    if (!executorService.awaitTermination(800, TimeUnit.MILLISECONDS)) {
        executorService.shutdownNow();
    }
} catch (InterruptedException e) {
    executorService.shutdownNow();
}

5. ИнтерфейсFuture

Методыsubmit() иinvokeAll() возвращают объект или коллекцию объектов типаFuture, что позволяет нам получить результат выполнения задачи или проверить статус задачи (выполняется ли она или выполнен).

ИнтерфейсFuture предоставляет специальный метод блокировкиget(), который возвращает фактический результат выполнения задачиCallable илиnull в случае задачиRunnable. Вызов методаget() во время выполнения задачи приведет к блокировке выполнения до тех пор, пока задача не будет выполнена должным образом и не будет доступен результат.

Future future = executorService.submit(callableTask);
String result = null;
try {
    result = future.get();
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

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

String result = future.get(200, TimeUnit.MILLISECONDS);

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

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

ИнтерфейсFuture также обеспечивает отмену выполнения задачи с помощью методаcancel() и проверку отмены с помощью методаisCancelled():

boolean canceled = future.cancel(true);
boolean isCancelled = future.isCancelled();

6. ИнтерфейсScheduledExecutorService

ScheduledExecutorService запускает задачи после некоторой предопределенной задержки и / или периодически. Еще раз, лучший способ создать экземплярScheduledExecutorService - использовать фабричные методы классаExecutors.

В этом разделе будет использоватьсяScheduledExecutorService с одним потоком:

ScheduledExecutorService executorService = Executors
  .newSingleThreadScheduledExecutor();

Чтобы запланировать выполнение отдельной задачи после фиксированной задержки, используйте методscheduled() дляScheduledExecutorService. Есть два методаscheduled(), которые позволяют выполнять задачиRunnable илиCallable:

Future resultFuture =
  executorService.schedule(callableTask, 1, TimeUnit.SECONDS);

МетодscheduleAtFixedRate() позволяет периодически выполнять задачу с фиксированной задержкой. Приведенный выше код задерживается на одну секунду перед выполнениемcallableTask.

Следующий блок кода выполнит задачу после начальной задержки в 100 миллисекунд, а после этого будет выполнять ту же задачу каждые 450 миллисекунд. Если процессору требуется больше времени для выполнения назначенной задачи, чем параметруperiod методаscheduleAtFixedRate(),ScheduledExecutorService будет ждать завершения текущей задачи перед запуском следующей:

Future resultFuture = service
  .scheduleAtFixedRate(runnableTask, 100, 450, TimeUnit.MILLISECONDS);

Если необходимо иметь фиксированную задержку между итерациями задачи, следует использоватьscheduleWithFixedDelay(). Например, следующий код гарантирует паузу в 150 миллисекунд между окончанием текущего выполнения и началом другого.

service.scheduleWithFixedDelay(task, 100, 150, TimeUnit.MILLISECONDS);

В соответствии с контрактами методовscheduleAtFixedRate() иscheduleWithFixedDelay(), периодическое выполнение задачи завершится по завершенииExecutorService или если во время выполнения задачи возникнет исключение.

7. ExecutorService vs. Fork/Join

После выпуска Java 7 многие разработчики решили, что фреймворкExecutorService должен быть заменен фреймворком fork / join. Однако это не всегда правильное решение. Несмотря на простоту использования и частое повышение производительности, связанные с разветвлением / объединением, также снижается степень контроля разработчика над одновременным выполнением.

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

Напротив,according to Oracle’s documentation, fork / join был разработан для ускорения работы, которую можно рекурсивно разбить на более мелкие части.

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

Даже несмотря на относительную простотуExecutorService, есть несколько распространенных ошибок. Подведем итог:

Keeping an unused ExecutorService alive: В разделе 4 этой статьи есть подробное объяснение того, как выключитьExecutorService;

Wrong thread-pool capacity while using fixed length thread-pool: Очень важно определить, сколько потоков потребуется приложению для эффективного выполнения задач. Слишком большой пул потоков приведет к ненужным накладным расходам только для создания потоков, которые в основном будут находиться в режиме ожидания. Слишком немногие могут заставить приложение казаться не отвечающим из-за длительных периодов ожидания для задач в очереди;

Calling a Future‘s get() method after task cancellation: Попытка получить результат уже отмененной задачи вызоветCancellationException.

Unexpectedly-long blocking with Future‘s get() method: Тайм-ауты следует использовать, чтобы избежать неожиданного ожидания.

Код для этой статьи доступен вa GitHub repository.