CompletionService in Java: Handling Multiple Tasks Efficiently

Illustration for CompletionService in Java: Handling Multiple Tasks Efficiently
By Last updated:

When dealing with multiple concurrent tasks in Java, it’s often challenging to retrieve the results as soon as they are ready, not just in the order they were submitted. That’s where CompletionService comes in — a hidden gem in Java’s java.util.concurrent package that makes concurrent task management both elegant and efficient.

In this tutorial, you'll learn everything you need to know about CompletionService, its benefits over plain ExecutorService, and how to use it in real-world applications with best practices and expert guidance.


🚀 Introduction

🔍 What is CompletionService?

CompletionService is an interface that:

  • Combines an Executor with a BlockingQueue for managing tasks.
  • Allows you to submit multiple Callable tasks and retrieve their results as they complete, not in submission order.

Analogy: Think of it as a kitchen with multiple cooks. Instead of waiting for dishes in the order requested, you serve them as soon as they’re ready.


🧠 Core Interfaces and Classes

public interface CompletionService<V> {
    Future<V> submit(Callable<V> task);
    Future<V> take() throws InterruptedException;
    Future<V> poll();
    Future<V> poll(long timeout, TimeUnit unit);
}

Most commonly used implementation:

ExecutorCompletionService<V>

🔧 Syntax and Code Example

Submit and Retrieve Results as They Complete

ExecutorService executor = Executors.newFixedThreadPool(3);
CompletionService<String> service = new ExecutorCompletionService<>(executor);

for (int i = 0; i < 5; i++) {
    final int taskId = i;
    service.submit(() -> {
        Thread.sleep((long)(Math.random() * 3000));
        return "Result from task " + taskId;
    });
}

for (int i = 0; i < 5; i++) {
    Future<String> future = service.take(); // Waits for the next completed task
    System.out.println("Completed: " + future.get());
}

executor.shutdown();

🔄 Thread Lifecycle Revisited

State Description
NEW Thread is created
RUNNABLE Ready to execute
BLOCKED/WAITING Awaiting resource
TERMINATED Task completed

With CompletionService, threads are managed by an underlying executor and results are retrieved as soon as they're TERMINATED.


💥 Java Memory Model & Visibility

  • Futures returned by CompletionService provide happens-before guarantees.
  • Results are visible across threads once Future.get() returns.

🔐 Coordination and Locking

  • Internally, ExecutorCompletionService uses a BlockingQueue<Future<V>>.
  • Thread-safe by design — no need for additional synchronized blocks.
  • Great for non-blocking parallel processing.

  • ExecutorService for task submission
  • Callable to return results from threads
  • Future to hold result or exception
  • BlockingQueue for result queue
  • CompletableFuture (for advanced flows)

🌍 Real-World Use Cases

  • Web scraping multiple URLs in parallel and processing the fastest responders first
  • Database queries to multiple replicas and using the quickest
  • Rendering tasks (e.g., images, PDFs) and showing results as they complete
  • Machine learning model ensemble where each model computes independently

🧠 CompletionService vs ExecutorService

Feature ExecutorService CompletionService
Submit & wait in order
Wait for any task
Results as they complete
Manages result queue

📌 What's New in Java Versions?

Java 8

  • Lambdas with Callable and Runnable
  • CompletableFuture for more complex async flows

Java 9

  • Flow API for reactive-style programming

Java 11

  • Performance improvements in common pool

Java 21

  • Virtual Threads — perfect for lightweight concurrent task execution
  • Structured Concurrency — manage concurrent flows as a group
  • Scoped Values — better alternative to ThreadLocal

⚠️ Common Pitfalls

  • Mixing submit() and execute() — only submit() returns a result
  • Forgetting to shutdown() the executor
  • Not handling exceptions inside tasks
  • Using take() without a loop can block indefinitely

✅ Best Practices

  • Use bounded thread pools for resource safety
  • Always shutdown executors using shutdown() or try-with-resources
  • Wrap future.get() in try-catch for exception safety
  • Avoid blocking operations within submitted tasks unless necessary

🧰 Multithreading Patterns

  • Future TaskFuture returned by submit()
  • Worker Thread → Pool of threads handles submitted jobs
  • Thread-per-message → CompletionService acts like message dispatcher
  • Producer-consumer → Result queue acts as consumer buffer

✅ Conclusion and Key Takeaways

  • CompletionService is ideal for handling many concurrent tasks where order doesn’t matter.
  • It simplifies polling for completed results.
  • Use ExecutorCompletionService with a custom thread pool for performance tuning.
  • Combine it with structured concurrency or virtual threads in Java 21 for modern scalable apps.

❓ FAQ: CompletionService in Java

1. How is CompletionService different from ExecutorService?

CompletionService wraps an Executor and provides an easy way to get results as they complete.

2. Can I cancel tasks submitted to CompletionService?

Yes, keep the returned Future and call cancel(true).

3. Is CompletionService thread-safe?

Yes — it handles synchronization internally.

4. What happens if I call take() and no task is done?

The method blocks until a result is available.

5. What’s the advantage over tracking Futures manually?

Less code, simpler design, and natural result ordering by completion.

6. Can it be used with virtual threads?

Yes — pair with Executors.newVirtualThreadPerTaskExecutor() in Java 21.

7. What if a task throws an exception?

The exception is captured in the Future and rethrown when calling get().

8. What queue does ExecutorCompletionService use?

A LinkedBlockingQueue<Future<V>> by default.

9. Is it suitable for CPU-bound or IO-bound tasks?

Both — depending on your executor configuration.

10. Can I submit Runnable instead of Callable?

Yes, but the result will be null unless wrapped in Executors.callable().


By using CompletionService, you simplify one of the most complex aspects of concurrent programming — waiting for tasks efficiently. It’s a must-have tool for every serious Java developer working with parallelism.