← Back to list

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.

Istiyak Ahamed Milon · 2026-04-04 03:56 · 5 claps · 3.0 min read
#java #multithreading #executorservice #completablefuture #thread-pool
Open on Medium ↗
Wiki topics: 📚 · Books & Reading

Mastering Java Multithreading (Part 2): Thread Pools, ExecutorService & CompletableFuture

Java Multithreading — ExecutorService and 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