Multi-Threading in Spring Boot using Executor Service

Akshaya Ramesh
5 min readFeb 1, 2023

--

Thread is a light weight component that executes a sequence of programmed instructions. Threads will run concurrently with other threads based on the cores assigned to the CPU and each thread has its own path of execution. The process of executing threads parallelly is called Multi-Threading. One way to acheive multi-threading is using Executor Service from Java’s Concurrent API.

Read more about Concurrent Api

Read more about Executor Service

Use case: Let’s take a scenario where we have to find the number of cars and trucks in a country and the sum of cars and trucks in state wise. That is, lets say there are 10k cars and 20k trucks in India and 3k cars and trucks are there in Tamil Nadu, 2k in Kerala and so on.

Assumptions: Database of any choice. Here I have considered MongoDB. Cars is a separate collection and Trucks is a separate collection maintained.

Request: /vehicleInfo
Response:
{
“total”: 30000,
“cars”: 10000,
“trucks”: 20000,
“statwiseSplit”: {
“tamilNadu”: 3000,
“kerala”: 2000

}
}

Brute force: Normally, this case can be solved in many ways. One is to take all the cars and trucks seperately from database and then combine both and then take the sum and statewise split. Other is to make a join between cars and trucks from DB layer and group it based on state. so, let’s compute the time taken for the request with our assumptions:
time taken to fetch all the cars from database — 800 ms
time taken to fetch all the trucks from database — 800 ms
time taken to group cars based on state — 500 ms
time taken to group trucks based on state — 500 ms
total time taken for the request — 2600ms

Using Executor service: The same logic can be achieved by creating a 2 separate thread to fetch cars and trucks details from database parallelly. Once we get the result of both, we group those based on state and compute the sum.

First let’s create a service VehicleService and getVehicleInfo() to implement our logic. This method returns our response object VehicleResponse. So, now our class looks like:

@Service
public class VehicleService {
public VehicleResponse getVehicleInfo() throws Exception{
}
}

Our first logic is to get the cars and trucks details from database. Let’s assume that we have created two separate repositories for cars and trucks.
Now, in our service class we will create 2 methods to get the corresponding cars and truck details.

@Service
public class VehicleService {
@Autowired
CarsRepository carRepo;
@Autowired
TrucksRepo truckRepo;
public VehicleResponse getVehicleInfo() throws Exception{
return null;
}
List<Cars> getCars(){
return carRepo.findAll(); //gets car details from mongo db
}
List<Trucks> getTrucks(){
return truckRepo.findAll(); //gets truck details from mongo db
}
}

With this done, we will create a executor service with 2 threads to be executed in concurrent. One thread will call getCars() and other thread will call getTrucks().

@Service
public class VehicleService {
@Autowired
CarsRepository carRepo;
@Autowired
TrucksRepo truckRepo;
public VehicleResponse getVehicleInfo() throws Exception{
ExecutorService pool = Executors.newFixedThreadPool(2); // creates 2 threads
Future<List<Cars>> carFuture = pool.submit(this::getCars); // assigning one thread to get car info from db
Future<List<Trucks>> trucksFuture = pool.submit(this::getTrucks); // assigning one thread to get truck info from db
}
List<Cars> getCars(){
return carRepo.findAll(); //gets car details from mongo db
}
List<Trucks> getTrucks(){
return truckRepo.findAll(); //gets truck details from mongo db
}
}

Now, we will wait till all the threads that we created are completed and will shutdown the executor service. So, as a result we will get the resultant of the threads.

@Service
public class VehicleService {
@Autowired
CarsRepository carRepo;
@Autowired
TrucksRepo truckRepo;
public VehicleResponse getVehicleInfo() throws Exception{
ExecutorService pool = Executors.newFixedThreadPool(2); // creates 2 threads
Future<List<Cars>> carFuture = pool.submit(this::getCars); // assigning one thread to get car info from db
Future<List<Trucks>> trucksFuture = pool.submit(this::getTrucks); // assigning one thread to get truck info from db

if(pool.awaitTermination(1, TimeUnit.HOURS)){ // waits till all the threds is completed and shutdown event is called
List<Cars> cars = carFuture.isDone()?carFuture.get():null; //get result of car thread
List<Trucks> trucks = trucksFuture.isDone()?trucksFuture.get():null; //get result of truck thread
}
}
List<Cars> getCars(){
return carRepo.findAll(); //gets car details from mongo db
}
List<Trucks> getTrucks(){
return truckRepo.findAll(); //gets truck details from mongo db
}
}

Since we have got the cars and trucks info, we will compute the state wise split. We can implement this in many ways, where I have introduced map concept to compute the state wise count. Our final service class looks like:

@Service
public class VehicleService {
@Autowired
CarsRepository carRepo;
@Autowired
TrucksRepo truckRepo;
public VehicleResponse getVehicleInfo() throws Exception{
VehicleResponse response = new VehicleResponse();
ExecutorService pool = Executors.newFixedThreadPool(2); // creates 2 threads
Future<List<Cars>> carFuture = pool.submit(this::getCars); // assigning one thread to get car info from db
Future<List<Trucks>> trucksFuture = pool.submit(this::getTrucks); // assigning one thread to get truck info from db
if(pool.awaitTermination(1, TimeUnit.HOURS)){ // waits till all the threds is completed and shutdown event is called
List<Cars> cars = carFuture.isDone()?carFuture.get():null; //get result of car thread
List<Trucks> trucks = trucksFuture.isDone()?trucksFuture.get():null; //get result of truck thread
Map<String,Integer> stateSplit = cars.stream()
.collect(Collectors.toMap(Cars::getState,y->1,(a,b)->a+b)); //state wise split for cars
trucks.stream().forEach(t -> {
Integer count = stateSplit.getOrDefault(t.getState(),0);
stateSplit.put(t.getState(),++count); // computing state wise split for trucks and adding it in the same map
});
response.setStatwiseSplit(stateSplit);
response.setTotal(cars.size()+trucks.size());
response.setCars(cars.size());
response.setTrucks(trucks.size());
return response;
}
}
List<Cars> getCars(){
return carRepo.findAll(); //gets car details from mongo db
}
List<Trucks> getTrucks(){
return truckRepo.findAll(); //gets truck details from mongo db
}
}

Contoller:

@RestController
public class VehicleController {
@Autowired
VehicleService vehicleService;
@GetMapping("/vehicleInfo")
public ResponseEntity<VehicleResponse> getVehicleInfo() throws Exception{
VehicleResponse res = vehicleService.getVehicleInfo();
if(res!=null) return ResponseEntity.ok(res);
else return (ResponseEntity<VehicleResponse>) ResponseEntity.noContent();
}
}

With the introduction of multiple thread, our computation time is:
executor threads computation — 800ms (time taken to fetch all the cars from database — 800 ms || time taken to fetch all the trucks from database — 800 ms )
time taken to group cars based on state — 500 ms
time taken to group trucks based on state — 500 ms
total time taken for the request — 1800ms

Conclusion

The goal of this article was to provide an implementation of multi-threading using executor service in spring boot. I hope it is informative and useful to you.

--

--