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 aBlockingQueue
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 aBlockingQueue<Future<V>>
. - Thread-safe by design — no need for additional
synchronized
blocks. - Great for non-blocking parallel processing.
⚙️ Related Concurrency Classes
ExecutorService
for task submissionCallable
to return results from threadsFuture
to hold result or exceptionBlockingQueue
for result queueCompletableFuture
(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
andRunnable
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()
andexecute()
— onlysubmit()
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()
ortry-with-resources
- Wrap
future.get()
in try-catch for exception safety - Avoid blocking operations within submitted tasks unless necessary
🧰 Multithreading Patterns
- Future Task →
Future
returned bysubmit()
- 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.