Mastering Java Multithreading (Part 2): Thread Pools, ExecutorService & CompletableFuture
In my previous post, “Mastering Java Multithreading: Thread vs Runnable vs Callable”, we explored how to create threads manually.
Mastering Java Multithreading (Part 2): Thread Pools, ExecutorService & CompletableFuture

Java Multithreading — ExecutorService and CompletableFuture
In my previous post, *“Mastering Java Multithreading: Thread vs Runnable vs Callable”*, we explored how to create threads manually.
But here’s the reality: In production systems, we rarely create threads directly.
Instead, we use Thread Pools, ExecutorService, and CompletableFuture to build scalable, maintainable, and high-performance applications.
In this post, we’ll cover:
- What Thread Pools are
- How to use ExecutorService
- Why thread pools matter in real-world systems
- How CompletableFuture simplifies async programming
- Practical Java examples you can use immediately
Why Not Create Threads Manually?
Creating threads manually (new Thread()) is fine for learning—but problematic in real applications:
- Expensive to create/destroy threads
- No control over how many threads run
- Can easily crash system (OutOfMemoryError)
- Difficult to manage lifecycle
This is where Thread Pools come in.
What is a Thread Pool?
A Thread Pool is a collection of reusable threads that execute tasks.
Instead of creating a new thread every time:
- Threads are created once
- Reused for multiple tasks
- Managed efficiently by the system
ExecutorService — The Backbone of Thread Pools
What is ExecutorService?
ExecutorService is a high-level API for managing threads and executing tasks asynchronously.
It abstracts:
- Thread creation
- Task scheduling
- Thread lifecycle
How to Use ExecutorService
Example: Fixed Thread Pool
import java.util.concurrent.*;
public class ExecutorExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 5; i++) {
int taskId = i;
executor.submit(() -> {
System.out.println("Executing task " + taskId +
" by " + Thread.currentThread().getName());
});
}
executor.shutdown();
}
}
When / Why to Use ExecutorService?
Use it when:
- You need to handle multiple concurrent tasks
- You want to limit number of threads
- You want better performance and resource management
- You are building APIs, microservices, batch jobs
Types of Thread Pools
Executors.newFixedThreadPool(5); // Fixed number of threads
Executors.newCachedThreadPool(); // Dynamic threads
Executors.newSingleThreadExecutor(); // Single worker thread
Executors.newScheduledThreadPool(3); // For delayed tasks
Callable + Future with ExecutorService
We briefly saw Callable before. Now let’s combine it properly with ExecutorService.
Example: Callable with Future
import java.util.concurrent.*;
public class FutureExample {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(2);
Callable<String> task = () -> {
Thread.sleep(1000);
return "Task Completed!";
};
Future<String> future = executor.submit(task);
System.out.println("Doing other work...");
String result = future.get(); // blocking
System.out.println(result);
executor.shutdown();
}
}
Why Use Future?
- Get result from async task
- Control execution
- Check status (isDone())
Problem: future.get() is blocking. So We enter CompletableFuture.
CompletableFuture — Modern Asynchronous Programming
What is CompletableFuture?
CompletableFuture is an advanced API introduced in Java 8 that allows:
- Non-blocking async programming
- Chaining multiple tasks
- Combining results
- Handling exceptions cleanly
Basic Example
import java.util.concurrent.*;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> {
return "Hello from async task!";
});
future.thenAccept(result -> {
System.out.println(result);
});
// Prevent main from exiting early
try { Thread.sleep(1000); } catch (Exception e) {}
}
}
Chaining Example
CompletableFuture.supplyAsync(() -> "Hello")
.thenApply(result -> result + " Java")
.thenApply(result -> result + " Developer")
.thenAccept(System.out::println);
Output:
Hello Java Developer
Combining Multiple Futures
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Java");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "Threading");
CompletableFuture<String> combined =
f1.thenCombine(f2, (a, b) -> a + " " + b);
combined.thenAccept(System.out::println);
When / Why to Use CompletableFuture?
Use it when:
- You want non-blocking code
- You need parallel API calls (very common in microservices)
- You want to chain dependent tasks
- You want cleaner async code than future
ExecutorService vs CompletableFuture
If we compare ExecutorService and CompletableFuture then we find that- Both ExecutorService and CompletableFuture has Thread management and both return value.
CompletableFuture is non-blocking and chaining easy where as ExecutorService is bolcking and hard to chaining.
ExecutorService is best for task execution where as CompletableFuture is best for Async workflows.
Real-World Use Case
As a backend (Java/Spring Boot) developer, you’ll commonly use:
ExecutorService
- Processing background jobs
- Bulk data processing
- Scheduled tasks
CompletableFuture
- Calling multiple APIs in parallel
- Aggregating responses
- Improving API response time
Final Thoughts
If Part 1 was about creating threads, this part is about managing them like a pro.
- Use ExecutorService for controlled concurrency
- Use Thread Pools for performance
- Use CompletableFuture for modern async programming
In this Multithreading series we covered:
Part 1: *Mastering Java Multithreading: Thread vs Runnable vs Callable* Part 2: Thread Pools, ExecutorService & CompletableFuture
Together, these concepts provide a strong foundation for Backend engineering with Java.
For backend engineers building high-scale systems, understanding these can help design applications that are faster, scalable and production-ready.
메타데이터
- post_id
- 4d1ea79cf856
- slug
- mastering-java-multithreading-part-2-thread-pools-executorservice-completablefuture-4d1ea79cf856
- url
- https://medium.com/@milon.istiyak/mastering-java-multithreading-part-2-thread-pools-executorservice-completablefuture-4d1ea79cf856
- canonical_url
- https://medium.com/@milon.istiyak/mastering-java-multithreading-part-2-thread-pools-executorservice-completablefuture-4d1ea79cf856
- author_url
- https://medium.com/@milon.istiyak
- status
- ok
- fetched_at
- 2026-07-24 04:20:45