ExecutorService vs CompletionService: In‑Depth Comparison and Practical Usage in Java
This article explains the differences between ExecutorService and CompletionService, demonstrates how to replace a simple Future‑based approach with CompletionService for faster result retrieval, analyzes the underlying source code, and outlines typical use‑cases such as load‑balancing and early‑return patterns in concurrent Java applications.
When you have several tasks (A, B, C, D) with varying execution times, a common approach is to submit them to an ExecutorService and collect the results sequentially using Future.get() . The article shows a typical Java snippet that creates a fixed‑size thread pool, submits each task, stores the returned Future objects in a list, and then iterates over the list to call get() on each future.
ExecutorService executorService = Executors.newFixedThreadPool(4);
List
futures = new ArrayList
>();
futures.add(executorService.submit(A));
futures.add(executorService.submit(B));
futures.add(executorService.submit(C));
futures.add(executorService.submit(D));
// iterate Future list, get each result
for (Future future : futures) {
Integer result = future.get();
// other business logic
}Using Future.get() blocks the thread until the specific task finishes, which can cause unnecessary waiting if one task is much slower than the others. To address this, the article introduces CompletionService (implemented by ExecutorCompletionService ) that decouples task submission from result consumption, allowing you to retrieve results in the order they complete.
ExecutorService executorService = Executors.newFixedThreadPool(4);
// ExecutorCompletionService is the only implementation of CompletionService
CompletionService executorCompletionService = new ExecutorCompletionService<>(executorService);
List
futures = new ArrayList
>();
futures.add(executorCompletionService.submit(A));
futures.add(executorCompletionService.submit(B));
futures.add(executorCompletionService.submit(C));
futures.add(executorCompletionService.submit(D));
for (int i = 0; i < futures.size(); i++) {
Integer result = executorCompletionService.take().get();
// other business logic
}The article highlights that the two implementations look similar, but CompletionService solves the critical flaw of Future.get() by returning the earliest completed task, which can dramatically reduce overall latency in high‑concurrency scenarios.
The core idea is that CompletionService works like a message queue: tasks produce results, and a consumer takes them from a blocking queue. The interface defines five methods ( submit , take , poll , etc.), and the only concrete class is ExecutorCompletionService .
Future
submit(Callable
task);
Future
submit(Runnable task, V result);
Future
take() throws InterruptedException;
Future
poll();
Future
poll(long timeout, TimeUnit unit) throws InterruptedException;Two constructors are provided for ExecutorCompletionService —one that creates an internal LinkedBlockingQueue and another that accepts a custom BlockingQueue . Both require an Executor (usually a thread pool) because the service cannot create raw threads.
private final Executor executor;
private final AbstractExecutorService aes;
private final BlockingQueue
> completionQueue;
public ExecutorCompletionService(Executor executor) {
if (executor == null) throw new NullPointerException();
this.executor = executor;
this.aes = (executor instanceof AbstractExecutorService) ? (AbstractExecutorService) executor : null;
this.completionQueue = new LinkedBlockingQueue<>();
}
public ExecutorCompletionService(Executor executor, BlockingQueue
> completionQueue) {
if (executor == null || completionQueue == null) throw new NullPointerException();
this.executor = executor;
this.aes = (executor instanceof AbstractExecutorService) ? (AbstractExecutorService) executor : null;
this.completionQueue = completionQueue;
}When a task is submitted, ExecutorCompletionService wraps it in a QueueingFuture (a subclass of FutureTask ) and executes it via the underlying executor. The overridden done() method of QueueingFuture adds the completed task to the completionQueue , enabling take() to return the earliest finished result.
public Future
submit(Callable
task) {
if (task == null) throw new NullPointerException();
RunnableFuture
f = newTaskFor(task);
executor.execute(new QueueingFuture(f));
return f;
}
protected void done() {
completionQueue.add(task);
}Typical usage patterns include collecting all results, stopping after the first non‑null result, or implementing a simple load‑balancer that picks the fastest service response. The article lists real‑world scenarios such as Dubbo’s Forking Cluster, multi‑source file downloads, and parallel weather‑service queries.
In summary, CompletionService provides a clean abstraction for “submit‑and‑consume‑as‑soon‑as‑ready” workflows, improving throughput and latency without complex synchronization code.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.