How I implemented Multithreading and reduced my API time from 4s to <100ms
An elaboration to a very basic problem I'd encountered
Before we start let's break this down into a few sections:
- Why did the problem occur?
- Solution that I implemented
- Thread-safety?
I've tried my best to make it as generic and language independent as possible but I've added some Java code since I worked on this project using Java-Spring Boot.
I implemented this on a production application from one of my projects.
Why did the problem occur?
To give a little bit of context, my API has 2 services and a repository called from a single service. Let's name them service1
, service2
, repository1
and mainService
.
The time to complete service1
and repository1
was <100ms whereas the time to complete service2
was about 3.5-4.5 seconds. Both these processes were quite important and I couldn't ditch the process carried by service2
.
For readability, let's name
service1
+repository1
asprocess1
andservice2
asprocess2
.
So, I took some time to figure out what was happening. process2
had very little logic(pretty much passing the parameters to the right method) and was primarily based off a library I was using. I started exploring the library and figured there was not much that could be done, since I figured the tasks it was executing were necessary.
But, I also noticed that both my processes(process1
and process2
) were independent of each other. An idea struck my head.
On a side note: the library is the JavaMailSender.
Solution that I implemented
All I had to do was somehow have process2
run on a separate thread and have my main thread return the result after process1
.
So I started exploring my options, I could use Java's CompletableFuture
class. This worked perfectly but since I was using Spring Boot, I wanted to check if there was any other way of pushing process2
into being a background task and I found a solution. This solution had its advantages:
- I could rely on 2 annotations to finish my task.
- Since I didn't want my caller thread to wait for any kind of response, I could use this.
If I had to work with a certain response, I would rather prefer using
CompletableFuture
.
So what were these annotations? It is a simple @EnableAsync
on top of my @SpringBootApplication
and an @Async
annotation on top of the method I was calling. You can read more about it here.
Java is synchronous. To achieve multithreading, java uses Asynchronous calls.
The @EnableAsync
annotation enables our spring boot application to process asynchronous code.
@EnableAsync
@SpringBootApplication
public class ClassName {
public static void main(String[] args) {
SpringApplication.run(ClassName.class, args);
}
}
Now let's add the @Async
annotation on top of the method that is being called(in this case, sendMailFromServer
). This allows running this method on a separate thread and as a background task when invoked.
@Async("emailThreadPool")
public void sendMailFromServer(){
//code
}
I created a new thread pool, and I provided the name in the attribute for
@Async
.
And ta-da we are done! ๐
Note: I also checked if the library I was using was thread-safe or not.
Thread-safety?
An obvious question that comes to our mind after this: Is this really thread-safe?
Let's understand this in a little detail.
Multithreading is really cool and helpful but it does come at a price, one very usual scenario is when multiple threads access the same resource, and if not handled well this can cause errors and might as well crash your entire application. Avoiding such situations by handling errors properly and by writing quality code creates a thread-safe application.
We can achieve the above said in the following ways:
- Stateless-implementations
- Immutable Implementations
- Thread-Local Fields
- By using Synchronized Collections or Concurrent Collections (Java)
- Using the synchronized keyword at the method level.
I hope this was helpful! I'm also attaching a few resources below for you to read if you'd like!