If you’re a Java developer, you probably know that you can create a thread by implementing the Runnable interface or by extending the Thread class. You can then execute multiple threads in parallel to achieve concurrency. Still with me?
It gets a bit more challenging when you need to spawn long-running tasks in parallel and then wait for all the tasks to finish. If you also need a return value from all those threads, we’re talking about a serious coding effort! However, Java has a clean, pre-built solution for this requirement: – Executors and Thread Pools. The java.util.concurrent.ExecutorService interface represents an asynchronous execution mechanism that is capable of executing tasks in the background.
A thread pool is represented by an instance of the class ExecutorService. With an ExecutorService, you can submit a task that will be completed in the future. Here are the types of thread pools you can create with the ExecutorsService class:
- Single Thread Executor: A thread pool with only one thread. All the submitted tasks will be executed sequentially. Method: Executors.newSingleThreadExecutor()
- Cached Thread Pool: A thread pool that creates as many threads as it needs to execute the tasks in parallel. The old available threads are reused for the new tasks. Method: Executors.newCachedThreadPool()
- Fixed Thread Pool: A thread pool with a fixed number of threads. If a thread is not available for the task, the task is put into a queue until another task ends and frees up a thread. Method: Executors.newFixedThreadPool()
- Scheduled Thread Pool: A thread pool made to schedule future tasks. Method: Executors.newScheduledThreadPool()
- Single Thread Scheduled Pool: A thread pool with only one thread to schedule a future task. Method: Executors.newSingleThreadScheduledExecutor()
Once you get an instance of the ExecutorService class, you can add tasks to it. A task can be one of the following:
- An object that implements the Callable interface and implements the call() method. Such a task can return a value to the parent thread.
- An object that implements the Runnable interface and implements the run() method. Such a task cannot return a value back to the parent thread.
ExecutorService can then execute the tasks using one of the two methods:
- execute: Only tasks that implement Runnable interface can be executed using this method. These tasks are just executed asynchronously. There is no way to obtain a result of the task, or to wait on the completion.
- submit: Both tasks, those that implement Runnable and that implement Callable, can be executed using this method. The returned Future<> object can be used to track the task.
You do need to shut down the pool in the end, using one of the ExecutorService’s shutdown methods, and it’s always a good idea to wrap it inside a finally block. You can shut down the ExecutorService using the method shutdown or shutdownNow. The shutdown() method will allow previously submitted tasks to execute before terminating, while the shutdownNow() method prevents waiting tasks from starting and attempts to stop currently executing tasks.
I’ll illustrate this simple solution with the help of a rudimentary example below using the Callable interface.