Java
ExecutorService
Concurrency
Multithreading
ThreadPool

ExecutorService, how to wait for all tasks to finish

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

ExecutorService is a substantial tool in Java's concurrent programming toolkit. It's part of the java.util.concurrent package, introducing a high-level API for managing threads. This allows developers to handle asynchronous task execution without wading into the complexities of directly dealing with low-level threading mechanisms. One of the essential features of ExecutorService is its ability to manage task execution and provide mechanisms to wait for tasks to complete.

What is ExecutorService?

ExecutorService is an interface in the Java Concurrency framework that provides a pool of threads to execute future tasks asynchronously. It abstracts away the creation, management, and teardown of threads, relieving developers from manual thread control.

Core Features

  • Thread Pool Management: Automates the creation and recycling of threads, improving application performance and resource management.
  • Task Submission: Allows for the submission of tasks that will be executed in the future (either once or repeatedly).
  • Completion Mechanisms: Provides methods to wait for tasks to complete or retrieve results.

How to Create an ExecutorService

There are several ways to create an ExecutorService. The most common is through the Executors utility class, which provides factory methods for different types of thread pools:

  1. Single Thread Executor:
java
   ExecutorService executor = Executors.newSingleThreadExecutor();
  1. Fixed Thread Pool:
java
   ExecutorService executor = Executors.newFixedThreadPool(5);
  1. Cached Thread Pool:
java
   ExecutorService executor = Executors.newCachedThreadPool();
  1. Scheduled Thread Pool:
java
   ScheduledExecutorService executor = Executors.newScheduledThreadPool(4);

Waiting for Tasks to Complete

When working with concurrent tasks, you often need to wait for all tasks to complete before proceeding. ExecutorService provides several strategies to accomplish this:

1. awaitTermination()

The awaitTermination(long timeout, TimeUnit unit) method is available in ExecutorService to wait for previously submitted tasks to finish execution.

Example:

java
1executor.shutdown(); // Disable new tasks from being submitted
2try {
3    // Wait a maximum of 60 seconds for tasks to complete
4    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
5        executor.shutdownNow(); // Cancel currently executing tasks
6        // Wait a further 60 seconds for tasks to respond to being cancelled
7        if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
8            System.err.println("Executor did not terminate");
9        }
10    }
11} catch (InterruptedException ie) {
12    executor.shutdownNow();
13    Thread.currentThread().interrupt();
14}

2. Using invokeAll()

invokeAll(Collection<? extends Callable<T>> tasks) blocks until all tasks have completed execution after a shutdown.

Example:

java
1List<Callable<Integer>> tasks = Arrays.asList(
2    () -> 1,
3    () -> 2,
4    () -> 3
5);
6try {
7    executor.invokeAll(tasks).forEach(future -> {
8        try {
9            System.out.println(future.get());
10        } catch (InterruptedException | ExecutionException e) {
11            e.printStackTrace();
12        }
13    });
14} catch (InterruptedException e) {
15    e.printStackTrace();
16}

Comparison of Waiting Techniques

MethodDescriptionUse Case
awaitTerminationWaits for the termination of tasks.Use when it's critical all tasks finish at shutdown.
invokeAllExecutes and waits for all tasks.When tasks are submitted in bulk and results matter.

Best Practices

  • Shutdown Gracefully: Always use shutdown() or shutdownNow() when done with the ExecutorService to free system resources.
  • Catch Exceptions Properly: Use try-catch blocks around awaitTermination() and task executions to handle interruptions and execution exceptions.
  • Select Appropriate Thread Pools: Choose thread pools based on workload characteristics, e.g., newFixedThreadPool for a steady number of tasks, newCachedThreadPool for a large number of short-lived asynchronous tasks.

Conclusion

ExecutorService dramatically simplifies thread management in Java through its various implementations and methods. Grasping these fundamentals enables you to effectively coordinate concurrent tasks and ensure that your application is both responsive and efficient. Whether you are shutting down a service or waiting for a set of tasks to complete, ExecutorService gives you the right tools to manage concurrency with ease.


Course illustration
Course illustration

All Rights Reserved.