Explain Codes LogoExplain Codes Logo

Java executors: how to be notified, without blocking, when a task completes?

java
async-programming
callback-functions
executor-service
Nikita BarsukovbyNikita Barsukov·Feb 26, 2025
TLDR

To get notified of your ExecutorService tasks completion without blocking, attach callbacks using CompletableFuture. Use thenRun for a no-argument callback or thenAccept for callbacks that work with the task result.

ExecutorService executor = Executors.newCachedThreadPool(); CompletableFuture<Integer> futureTask = CompletableFuture.supplyAsync(() -> 42, executor); futureTask.thenAccept(result -> System.out.println("Result: " + result));

In this snippet, we create a task to return 42, submit it with the ExecutorService, and then thenAccept is used to print the result once it's available — all without blocking the main thread.

Supercharging with CompletableFuture

Completing tasks asynchronously isn't just about notifications — it's about having more control and flexibility. Let's explore:

Attaching simple callbacks to tasks

When a task completes, you may want to execute an action. In this case, thenRun or thenAccept can be used to chain tasks.

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { // Task logic: it's just business as usual here... }).thenRun(() -> { // Callback logic: Party starts here after business hours 🎉 });

Multi-tasking with callbacks

When tasks are interdependent, thenCompose or thenCombine chains them together.

CompletableFuture<String> future = CompletableFuture .supplyAsync(() -> "Hello") .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + " World")); // World can't exist without a Hello, can it?

Handling exceptions in callbacks

exceptionally allows us to recover from exceptions, ensuring the task flow is not interrupted.

CompletableFuture<Integer> future = CompletableFuture .supplyAsync(() -> { throw new RuntimeException("Oops!"); }) .exceptionally(ex -> 0); // When life gives us RuntimeException, we make it ZeroException! 😁

Instant callback on task completion

Override the done() method by extending FutureTask, and voila! You've got a hook into the completion event:

FutureTask<Integer> futureTask = new FutureTask<>(() -> 42) { @Override protected void done() { // Callback code: Time to celebrate! 🎉 } }; executor.execute(futureTask);

Expanding your arsenal with callbacks

The power of Guava's ListenableFuture

Guava's ListenableFuture and ListeningExecutorService provide you with control you didn't know you needed. Using them, you can register callbacks:

ListeningExecutorService service = MoreExecutors.listeningDecorator(executor); ListenableFuture<Integer> future = service.submit(() -> 42); Futures.addCallback(future, new FutureCallback<Integer>() { public void onSuccess(Integer result) { // Victory dance 💃💃💃 } public void onFailure(Throwable thrown) { // Console crying 🤡🤡🤡 } }, service);

ThreadPoolExecutor hooks for intelligence

Gain valuable insights or take actions pre or post task execution with ThreadPoolExecutor hooks, beforeExecute and afterExecute :

ThreadPoolExecutor executor = new ThreadPoolExecutor(...) { @Override protected void beforeExecute(Thread t, Runnable r) { // Last-minute briefing } @Override protected void afterExecute(Runnable r, Throwable t) { // Post-battle debrief } };

Consistent performance with fixed thread pool

Ensure predictable performance by using a fixed thread pool.

ExecutorService fixedExecutor = Executors.newFixedThreadPool(10); // Now, you are a productivity monster! 😎

Taking action after an ExecutorService terminations

Perform actions after the ExecutorService has been shut down by overriding terminated():

ThreadPoolExecutor executor = new ThreadPoolExecutor(...) { @Override protected void terminated() { // ExecutorService is no more. Long live ExecutorService! 👻 } };

Artfully crafting tasks and callbacks

Keeping your asynchronous tasks and callbacks well-organized will bolster maintainability:

  • Use the Callable interface to define tasks promising tasty return values.
  • Release resources religiously in tasks using a try-finally structure.
  • Chain operations with thenApply for taking a gander at results.
  • Keep chain intact by healing a broken link with exceptionally when there's an exception.