In modern applications, we often need to fetch data from different web services, query databases, or perform heavy calculations. If we run these tasks one after another, our application becomes slow. To solve this, Java introduced CompletableFuture in Java 8. It allows us to run multiple tasks in the background concurrently and combine their results when they are ready.
But how does it work, and how do we coordinate tasks that depend on one another? Let's break this down using a simple cooking analogy.
Imagine you want to make a gourmet chicken sandwich for dinner. This requires two main tasks:
- Bake the sandwich bread in the oven.
- Grill the chicken breast on the stove.
If you bake the bread first, stand there waiting for it to finish, and only then start grilling the chicken, your dinner will take twice as long. This is synchronous (blocking) execution.
Instead, you put the bread in the oven (Task A) and immediately start grilling the chicken on the stove (Task B) at the same time. Both tasks are running in the background. This is asynchronous execution.
Once the bread is fully baked AND the chicken is perfectly grilled, you combine them together to assemble your sandwich (this is thenCombine). If either one is still cooking, you wait. When both are done, dinner is served!
How This Looks in Java Code
In Java, we use CompletableFuture.supplyAsync() to start background tasks. We can then chain them together using thenCombine() to merge their results.
package io.practise.threads;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class TestCompletableFuture {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// Start baking the bread in the background (FirstThread)
CompletableFuture first = CompletableFuture.supplyAsync(FirstThread::new);
// Start grilling the chicken in the background (SecondThread)
CompletableFuture second = CompletableFuture.supplyAsync(SecondThread::new);
// thenCombine: Wait for both to complete, then assemble the sandwich
CompletableFuture<Void> voidCompletableFuture = first.thenCombine(second, (o, o2) -> {
return o.toString() + " " + o2.toString();
});
// Fetch the final combined result
System.out.println(voidCompletableFuture.get());
}
}
Breaking Down the Code
- CompletableFuture.supplyAsync(...): This tells Java to execute the task asynchronously on a separate thread in the background, like setting a timer on the oven.
- thenCombine(otherFuture, combinationFunction): This joins two independent futures together. It says: "Once I'm done, and the other future is also done, pass both of our results into this function and return the combined result."
- .get(): This is the final step. It blocks the program and waits for the combined future to finish, delivering the final product.
Traditionally, writing multithreaded code involved messy callbacks, manual synchronization, and complex error handling. CompletableFuture provides a clean, readable API that lets you chain actions together naturally (using a functional style) without blocking your main program thread.
Conclusion
Java's CompletableFuture makes writing concurrent, non-blocking code elegant and maintainable. By allowing tasks to run independently in the background and combining them with methods like thenCombine(), you can build super-fast applications that handle multiple tasks efficiently and smoothly.