Skip to content
Lesson 2 of 7

Step 1 of 7 · Reading · ~3 min

Learn

Concurrency

Executors and CompletableFuture

Raw Thread objects make you create, start and join every worker by hand. An executor separates the work (a task) from the machinery that runs it, and CompletableFuture lets you describe what happens with the result before it exists. You will be able to run tasks on a pool, chain stages, and recover from a failure in the middle of a chain.

Pools, tasks and futures

An ExecutorService owns a fixed set of worker threads and a queue. Submitting more tasks than threads is normal: the extras wait in the queue.

ExecutorService pool = Executors.newFixedThreadPool(3);
Callable<Integer> job = () -> Integer.parseInt("42") * 2;    // Callable returns a value, may throw
Future<Integer> f = pool.submit(job);
int v = f.get();                // blocks until done: 84
pool.shutdown();                // stop accepting work; queued tasks still run

Runnable returns nothing and cannot throw a checked exception; Callable<V> does both. If a task throws, get() rethrows it wrapped in an ExecutionException; the original is e.getCause(). To run many tasks and wait for all, pool.invokeAll(listOfCallables) returns the futures in the order you gave the tasks, not the order they finished.

Pool threads are not daemon threads. If you forget shutdown(), the JVM keeps running after main returns.

Chaining stages

A CompletableFuture<T> is a future you can attach steps to. Each step returns a new future, so a chain reads top to bottom:

CompletableFuture<String> label =
    CompletableFuture.supplyAsync(() -> Long.parseLong("9000000000"), pool)   // stage 1, on the pool
        .thenApply(big -> big / 1000)                                         // map the value
        .thenApply(k -> k + " thousand");
MethodUse it when
thenApply(fn)fn returns a plain value (map)
thenCompose(fn)fn itself returns a CompletableFuture (flat-map)
thenCombine(other, fn)you need the results of two independent futures
CompletableFuture.allOf(a, b, c)you only need to know when all of them are done

Pass the pool as the last argument. Without it, supplyAsync uses the shared ForkJoinPool.commonPool(), which is sized for CPU work and is also used by parallel streams.

Failure travels down the chain

If any stage throws, every later thenApply is skipped and the failure arrives at the first stage that handles it. Two details catch everyone:

CompletableFuture<Integer> g =
    CompletableFuture.supplyAsync(() -> Long.parseLong("12x"), pool)   // throws NumberFormatException
        .thenApply(n -> n * 2)                                         // skipped
        .handle((n, err) -> err == null ? 1 : -1);

The err that handle receives is a CompletionException whose getCause() is the NumberFormatException; the original is wrapped once. Printing err.getMessage() gives java.lang.NumberFormatException: ... and err.getClass() gives the wrapper, which is rarely what you want. handle runs for success and failure (one of the two arguments is null); exceptionally runs only on failure; whenComplete observes without changing the result.

Second: allOf(...).join() throws if any input failed. Recover each future first (with handle) and then wait on the recovered ones, or the one bad item takes down the whole batch.

join() throws the unchecked CompletionException; get() throws the checked ExecutionException. They carry the same cause.

Common mistakes

  • Reading err.getMessage() or err.getClass() directly in handle. You print CompletionException or the cause's class name with a package prefix instead of the real exception.
  • Calling get() inside the loop that submits. Each call blocks, so the pool runs one task at a time. Submit everything, then collect.
  • Forgetting shutdown(). The program prints its result and then never exits.
  • thenApply where the function returns a future. You get CompletableFuture<CompletableFuture<T>> and the inner failure is never seen.

Your exercise

Complete run, which turns one line of input into a future of one output line, handling both a bad number and a division by zero. The reading, the pool and the printing are in the starter. The likely mistake is reporting the wrapper exception's name instead of the real one.

Up nextReflection and AnnotationsReflection and JVM

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…