Executing operations one after another is easy. However, sometimes we desire more - parallelism. Are you sure you know how to execute tasks in parallel in Java? If not, let me show you how to start multiple threads with ExecutorService, give them some tasks and wait for the result.
One thread execution
A sample problem that I will solve is to sum all integers from 1 to 100. That is extremely easy job to do. Although there multiple approaches, I will use one of them. This task can be done by one simple method.
private int sum(int from, int to) {
return IntStream.rangeClosed(from, to).sum();
}
It uses Java streams to sum the integers from a specific range. IntStream.rangeClosed creates a stream of consecutive integers from a range. The sum operation sums all numbers from the stream and returns the total.
To print a sum of integers from 1 to 100, I run the following code:
System.out.println(sum(1, 100));
The result is 5050
assertEquals(5050, sum(1, 100));
but it does not matter.
Splitting one task to smaller independent tasks
Of course, that was easy. But we can play more with this example. Assuming that summing integers is tedious, we can split that work to smaller jobs. For example, the sum of integers 1 to 100 equals a sum of subtotals.
sum(1, 100) = sum(1, 19) + sum(20, 39) + sum(40, 59) + sum(60, 79) + sum(80, 100)
One bigger task was split to multiple smaller tasks which can be independently done in any order.
Multiple threads in Java
Now we can spread those subtasks to separate threads. It can be done in a few ways in Java but this time I will use ExecutorService.
A big advantage of using it is a fact that ExecutorService is available in JDK API, so you do not need any additional library or a framework.
At the beginning, the executor service has to be instantiated. In this example, I create it as a fixed size pool of five threads.
Then I create a list of tasks by creating objects that implement Callable interface. When the list is ready, executorService runs all those tasks using the pool of threads - it is done by invokeAll method.
ExecutorService executorService = Executors.newFixedThreadPool(5);
List<Callable<Integer>> subTasks = List.of(
() -> sum(1, 19),
() -> sum(20, 39),
() -> sum(40, 59),
() -> sum(60, 79),
() -> sum(80, 100)
);
List<Future<Integer>> results = executorService.invokeAll(subTasks);
invokeAll returns a list of results. Each result is wrapped by an object implementing Future interface. We will get back to that.
Wait for all threads to finish
Obviously, we are missing summing up all subtotals to get the total sum of integers 1-100. Theoretically, we should wait on all threads to complete calculations and then add up all results according to the below equation.
sum(1, 100) = sum(1, 19) + sum(20, 39) + sum(40, 59) + sum(60, 79) + sum(80, 100)
But how can we make sure that all threads finished? As we used invokeAll method, nothing else is required. Once invokeAll finishes, results from all threads are available. The method starts all threads from subTasks list and waits until they all finished.
A complete code of a junit test which spreads calculation between 5 threads, waits for the result and aggregates them to the total is presented below.
@Test
void shouldExecuteInParallel() throws InterruptedException, ExecutionException {
ExecutorService executorService = Executors.newFixedThreadPool(5);
List<Callable<Integer>> subTasks = List.of(
() -> sum(1, 19),
() -> sum(20, 39),
() -> sum(40, 59),
() -> sum(60, 79),
() -> sum(80, 100)
);
List<Future<Integer>> results = executorService.invokeAll(subTasks);
int total = 0;
for (Future<Integer> result : results) {
total += result.get();
}
assertEquals(5050, total);
}
You may notice that each particular result is read using get method because it is not just a number, it is a Future object.
As you can see, ExecutorService is useful for executing multiple tasks in parallel without diving deep into technicalities.