Async in Java

Cognizant
Cognizant Softvision Insights
6 min readFeb 8, 2021

A tell-all guide to understanding the function

By Cristina Manitiu, Sr. Software Engineer, Cognizant Softvision

Asynchronous programming is a vast topic, one that has been widely discussed, yet software engineers still look for ways to implement and integrate it into applications.

As a Senior Software Engineer, I’ve found myself being curious about how I can do many things at once and I believe that I am not the only one asking this question. Everyone aspires to be more productive and wants the same from various applications.

Shifting our focus to asynchronous in Java, we will discover many ways to implement it and the different use cases for it in this article.

vs Asynchronous

Synchronous (Sync) and asynchronous (Async) programming can be done in one or multiple threads. The main difference between the two is when using synchronous programming we can execute one task at a time, but when using asynchronous programming we can execute multiple tasks at the same time. For example:

Sync:

  • Single thread: I start to boil an egg, after it is boiled I can start to toast the bread. I have to wait for a task to be done in order to start another.
  • Multi-thread: I start to boil an egg, after it is boiled my mom will toast the bread. The tasks are performed one after another and by different persons(threads).

Async:

  • Single thread: I put the egg to boil and set a timer, I put the bread to toast and start another timer, when they are done, I can eat. In asynchronous I don’t have to wait for a task to be done in order to start one other task.
  • Multi-thread: I hire 2 cooks and they will boil the egg and toast the bread for me. They can do it at the same time, neither they have to wait for one to finish in order for the other one to start.

Async with threads

The first way to implement async in Java is to use the Runnable interface and Thread class which is found from JDK 1.0. Any class can implement Runnable and override the run() method or can extend Thread and do the same. The difference is when the run method is called directly from a Runnable there won’t be a new thread created, instead it will run on the thread which is calling. However, when we do a thread.start() a new thread will be created.

For a better management of threads in JDK 1.5 we can find Executors. They use different ThreadPools and are used because there’s no need to manually create a thread. Instead, we can specify how many threads it can have and it will reuse these threads during the lifetime of the application.

Async with Future

run() is a void method and it can’t return any result from a thread, but if we need the result of a computation happening on a different thread than main we will need to use Callable interface. The response from a task is not available immediately and Callable will alternatively return a Future object when it is submitted to an ExecutorService. This object is the promise that when our computation is over we can get the result through it, we only have to call get() on Future. This is not a great usage of async since get() is blocking the current thread until the response is available. There is however, a workaround for this by using future.isDone() method to continuously check if the computation is over and only when this method returns true will get() return the result.

Async with CompletableFuture

In JDK 1.8, the Future object got an upgrade and it became a CompletableFuture object which besides Future, also implements CompletionStage. The CompletionStage offers a lot of methods for a simplified usage of the responses computed in different threads and stages. Some of the most common are thenApply() similar with map() from Streams, thenAccept() similar with foreach. There are multiple ways to get a CompletableFuture response, some of them running the task on a different thread or not, but one common point and extra functionality from Futures is that users can treat exceptions if they occur during computation.

Async with @Async

Another way to implement async is to use the annotation @Async provided by Spring Framework. Users can use it only on public methods and can’t call the methods from the same class where they are defined. Any code that is inside a method annotated with Async will be executed in a different thread and can be void or can return a CompletableFuture. So this an alternative to creating a CompletableFuture and giving it a method to run, but in order to be able to use this annotation, another thing is needed: @EnableAsync on a configuration class.

Spring Events

Using Spring Events for async implementation is a step forward which also offers decoupling and an easy way to add new functionality without changing the existing one. Three elements are needed:

  • An Event — any object that extends ApplicationEvent can be used
  • A publisher — a bean which will publish the event using ApplicationEventPublisher bean
  • A Listener — a bean which will have a method annotated with @EventListener and will assist in defining the task to be executed when a specific event is received

By default the listener method is executed synchronously, but can easily be made asynchronous by also adding the @Async annotation.

Another way to make the listener asynchronous is to add in the configuration a bean with a SimpleApplicationEventMulticaster and assign a TaskExecutor to it. When this bean is in place, annotating every EventListener with @Async is unnecessary, and all the events will be treated on a different thread. It would be helpful to use the configuration bean method when we don’t want to miss an @Async annotation on a method, but be aware that this approach will make asynchronous all the event treatment, even the framework events. Using the annotation will allow us to choose which events to be treated synchronous and which asynchronous.

Microservices

At the microservices level we can also have a synchronous or asynchronous communication. The difference between them is that, as stated in the definition, asynchronous means we don’t wait for an immediate response from the service we are calling, while synchronous means we wait for the response.

One of the most popular synchronous communications between microservices is done using REST calls. For the asynchronous communication we can use queues or topics. Both of them contain messages, but one difference would be that a message from a queue can be consumed only by one subscriber and a message from a topic can be read by multiple subscribers.

Pros and cons of asynchronous

Users should think about using asynchronous programming when wanting to delegate a task to another thread because it is time consuming or they don’t want the result of a task to impact the current flow of the application. In this way users can do multiple things at once. By using asynchronous users can decouple tasks and components and it results in a better overall performance of the application.

Alternatively, users should be aware that debugging is a little more complicated in a code with asynchronous methods and even writing tests, but this shouldn’t be an impediment when choosing a solution.

One last thing

Threads are about workers and async is about tasks!

--

--