Асинхронность в Java
Асинхронное программирование — обширная и получившая широкое обсуждение тема, но инженеры-программисты все еще ищут, как лучше реализовать эту идею и интегрировать в приложения.
Мне — старшему инженеру-программисту — стало любопытно, как возможно делать несколько вещей одновременно, и задаюсь этим вопросом наверняка не только я. Каждый стремится быть более продуктивным и хочет того же от своих приложений.
Переключив внимание на асинхронность в 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. Для асинхронной связи можно воспользоваться очередями или темами. И те, и другие содержат сообщения, но отличие в том, что сообщение из очереди может быть обработано только одним подписчиком, а сообщение из темы может быть прочитано несколькими подписчиками.
Плюсы и минусы асинхронности
Об асинхронном программировании стоит задуматься, когда вы хотите делегировать задачу другому потоку, поскольку она отнимает много времени, или не хотите, чтобы результат задачи влиял на текущий поток приложения. Так получится выполнять несколько операций одновременно. Используя асинхронность, вы можете разделять задачи и компоненты, что приводит к повышению общей производительности приложения.
В качестве альтернативы необходимо знать, что в коде с асинхронными методами усложняются отладка и написание тестов, но это не должно становиться препятствием при выборе решения.
И последнее
Потоки — это о работниках (воркерах), а асинхронность — это о задачах!
Async Programming in Java: Part I
Join the DZone community and get the full member experience.
As a backend engineer, we face situations to process the data asynchronously. Today let’s see how it’s done in java and various way to do it.
Starting from Thread, Runnable, Callable, Future (and its extended ScheduledFuture), CompletableFuture, and of course, ExecutorService and ForkJoinPool. We will see all of them, but one by one.
Thread
The very basic yet so powerful component of Java concurrency is Thread . The Thread of Java is actually associated with the Thread of the Operating System. The very basic way to create a Thread is by extending it and overriding the run method:
public class TestThread extends Thread
TestThread t = new TestThread();
t.start();// starting the thread, causes the run method be called
Starting the thread causes the run() method to be called.
You may ask; yes, Thread has tons of other methods that can be overridden:
- In most cases, we don’t want to override other methods of the thread.
- Once we extend the Thread class, the extending class losses its ability to extend further as Java does not support multiple inheritances.
- Each thread has its own object when we extend it, and it’s not good for memory health when there are tons of Objects of the extended Thread created.
Java addresses these issues with the Runnable interface. In fact, Thread has an overloaded method that takes Runnable.
Runnable
Runnable is an interface that has only one method: run() . Yes, Runnable is a functional interface, and its instance can be created with the lambda function. Yet it’s an easy way to do this; for complex things, we might like to implement it. See the difference here. It’s all about the requirement:
Runnable runnable = ()->System.out.println("I'm a runnable from lambda.");
// With implemention, we can hold the data and related stuff that we want to process.
// Otherwise we got to manage them in the launching thread
public class RunnableImplemented implements Runnable
ListObject> mayBeAListOfData;
public RunnableImplemented(ListObject> mayBeAListOfData,
Object mayBeAService, Object mayBeADao)
this.mayBeAListOfData = mayBeAListOfData;
this.mayBeAService = mayBeAService;
Though Runnable has a run() method, it’s not a Thread but just a Java class until it’s taken control by (passed to) Thread. The starting of the thread causes the runnable object’s run() method to be called.
private static Runnable runnable = ()->System.out.println("I'm a runnable from lambda.");
public static void main(String[] args)
Thread t = new Thread(runnable);// takes runnable here
Cool, we learnt how to create threads with Thread as well as Runnable. But have you noticed Thread (or Runnable either),
- The Runnable method doesn’t return anything?
- Also doesn’t have exception handling. You have to surround your code that throws an Exception with try and catch block
Yep, Java got it solved in version 1.5, and it’s Callable.
Callable
Callable is a generic interface. Why? The type of the return value as the generic type. Callable is too a functional interface and call() is the only method, a no-argument method that throws Exception and returns generic type value.
The implementing Callable is very similar to Runnable:
private static CallableInteger> callable = ()->
String data = "I'm in callable.";
public static void main(String[] args)
ExecutorService executor = Executors.newSingleThreadExecutor();
FutureInteger> callFuture = executor.submit(callable);
Integer integer = callFuture.get();
See here, the call method processes the data and returns a value that can be collected post-execution. But is there a huge difference in invoking it? We use ExecutorService to invoke and Future to hold the result. Let’s talk about why.
As you can see, there is no controlled behavior of creating and running the Threads (Runnable or Callable too). We may want to control the number of threads running at a time as each of them associated with OS’s threads. The number of Threads we run should be lesser than the number of available CPU cores. All together, Java solves it by ExecutorService interface.
I think it’s really a lot for today. Maybe in Part II, we will discuss Executors and different types of ExecutorService, and the Future interface.
Opinions expressed by DZone contributors are their own.