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

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

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

Синхронность vs асинхронность

Синхронное (Sync) и асинхронное (Async) программирование может выполняться как в одном, так и в нескольких потоках. Основное различие между в том, что при синхронном программирования мы выполняем одну задачу за раз, а при асинхронном программировании  —  несколько задач выполняются одновременно. Например:

Синхронность:

  • Однопоточность: я начинаю варить яйцо. После того как оно сварится, я могу начать поджаривать хлеб. Мне приходится ждать завершения одной задачи, чтобы начать другую.
  • Многопоточность: я начинаю варить яйцо, а после того как оно сварится, моя мама поджарит хлеб. Задачи выполняются одна за другой и разными лицами (потоками).

Асинхронность:

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

Асинхронность с потоками

Первый способ реализовать асинхронность в Java  —  воспользоваться интерфейсом Runnable и классом потока Thread, который доступен начиная с JDK 1.0. Любой класс может реализовать Runnable и переопределить метод run() либо расширить класс Thread и сделать то же самое.

Разница в том, что когда метод run вызывается непосредственно из Runnable, не создается новый поток, а метод выполняется в потоке, откуда вызван. Однако, если мы воспользуемся thread.start(), будет создан новый поток.

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

Асинхронность с Future

run()—  это void-метод, и он не может ничего возвращать из потока, но если нам нужен результат вычисления, выполняемого в другом потоке, чем main, то нужно будет воспользоваться интерфейсом Callable. Ответ от задачи недоступен немедленно, и в качестве альтернативы Callable вернет будущий объект Future, когда он будет отправлен в службу выполнения. Этот объект обещает, что, когда вычисления завершатся, мы получим их результат  —  достаточно только вызвать get(). Это не очень хорошее применение асинхронности, так как get() блокирует текущий поток до тех пор, пока не получит ответ. Однако существует обходной путь через метод future.isDone()  —  он постоянно проверяет, завершено ли вычисление, и только когда этот метод вернет значение true, get() возвратит результат.

Асинхронность с CompletableFuture

В JDK 1.8 объект Future получил обновление и стал объектом CompletableFuture, который, помимо будущего объекта, также реализует этап завершения (CompletionStage). CompletionStage предлагает множество методов для упрощения работы с ответами, вычисленными в разных потоках и этапах. Некоторые из наиболее распространенных  —  это thenApply(), аналогичная функции map() из потоков, а также thenAccept(), аналогичная функции foreach. Существует несколько способов получить ответ CompletableFuture. Одни выполняют задачу в другом потоке, другие нет, но их объединяет одно  —  если во время вычисления возникнут исключения, пользователи могут обрабатывать их.

Асинхронность с @Async

Другой способ реализации асинхронности  —  аннотация @Async из Spring Framework. Ее можно задействовать только в публичных методах, и в этом случае вызов методов из того же класса, в котором они определены, будет недоступен. Любой код, находящийся внутри метода с аннотацией @Async, будет выполняться в другом потоке и может быть недействительным или возвращать CompletableFuture. Таким образом, это альтернатива созданию CompletableFuture и предоставлению ему метода для запуска, но чтобы иметь возможность использовать эту аннотацию, необходимо другое: @EnableAsync в классе конфигурации.

События Spring

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

Необходимы три элемента:

  • Событие (Event)  —  может быть любым объектом, расширяющим ApplicationEvent.
  • Издатель (Publisher)  —  компонент, который опубликует событие с помощью компонента ApplicationEventPublisher.
  • Прослушиватель (Listener)  —  компонент, который содержит метод с аннотацией @EventListener, и помогает определить задачу, которая выполнится при возникновении определенного события.

По умолчанию метод прослушивателя выполняется синхронно, но это легко изменить, применив аннотацию @Async.

Другой способ сделать прослушиватель асинхронным  —  добавить в конфигурацию компонент с SimpleApplicationEventMulticaster и назначить ему TaskExecutor. Когда этот компонент на месте, не нужно аннотировать каждый список событий с помощью @Async, и все события будут сами обрабатываться в другом потоке. Если не хочется пропустить аннотацию @Async для какого-то метода, будет полезно воспользоваться этим способом, но имейте в виду: такой подход сделает асинхронной обработку всех событий, даже событий фреймворка. Использование аннотации позволит нам выбрать, какие события будут обрабатываться синхронно, а какие асинхронно.

Микросервисы

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

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

Плюсы и минусы асинхронности

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

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

И последнее

Потоки  —  это о работниках (воркерах), а асинхронность  —  это о задачах!

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи: Cognizant Softvision,Async in Java

Предыдущая статьяКак создавать надежные SSL-сертификаты для локальной разработки
Следующая статьяКак сделать калькулятор на C