Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Java ExecutorCompletionService: consume finished tasks, not submission order

Last updated: 5 Oct 20264 min read
tutorial
IntermediateBy AITrove Editorial

ExecutorCompletionService submits work to an owned executor and queues completed Future results in completion order.

Wait for the result that exists

Reading a List<Future> in submission order can block on a slow first task while a later task is ready. A completion service separates submission from result consumption. The fixture holds the first task behind a latch, then submits a second task that completes; take returns the second result first. It releases the first task before taking its result.

This ordering is an availability choice, not an ordering guarantee for a report. If the final output must match request order, retain a stable identifier and reorder after collecting. A failed task still yields a completed Future; get throws its ExecutionException, so a consumer must decide whether to continue or stop.

Own every submitted task

The completion queue can retain finished results until the caller drains them. Submitting an unbounded stream without draining or limiting admission can grow memory even with a fixed worker count. The program submits exactly two tasks, collects exactly two results, and shuts down its executor in finally.

Executor ownership covers shutdown, future composition handles dependency chains, and timeouts do not necessarily stop a worker. Those are different contracts.

Working program

Java
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class FinishedDispatchFirst {
    public static void main(String[] args) throws Exception {
        ExecutorService workers = Executors.newFixedThreadPool(2);
        CountDownLatch slowStarted = new CountDownLatch(1);
        CountDownLatch releaseSlow = new CountDownLatch(1);
        try {
            ExecutorCompletionService<String> completed = new ExecutorCompletionService<>(workers);
            completed.submit(() -> {
                slowStarted.countDown();
                if (!releaseSlow.await(2, TimeUnit.SECONDS)) throw new IllegalStateException("release deadline");
                return "slow-47";
            });
            if (!slowStarted.await(2, TimeUnit.SECONDS)) throw new IllegalStateException("start deadline");
            completed.submit(() -> "fast-82");
            System.out.println(completed.take().get());
            releaseSlow.countDown();
            System.out.println(completed.take().get());
        } finally {
            releaseSlow.countDown();
            workers.shutdown();
            if (!workers.awaitTermination(2, TimeUnit.SECONDS)) workers.shutdownNow();
        }
    }
}

Output

Output
fast-82
slow-47

Cost and ownership

With a fixed two-task fixture, work scheduling is bounded. For n submitted tasks, the completion service can retain O(n) completed Future references until consumption. The executor's queue and the completion queue are distinct memory owners, so a production producer needs an admission limit.

Common Mistakes

  • Do not read submitted Future objects in input order when the goal is first available output.
  • Do not forget to drain completed results or bound submissions.
  • Do not treat a completed Future as a successful task without calling get.

Read next

Java ExecutorService: bounded admission and shutdown, Java CompletableFuture: composition, failures and executor ownership, Java CompletableFuture timeout: completion is not worker cancellation, Java bounded executors: test saturation and rejected work.

java
completion-service-order
Storage details