Instantiating Threads DirectlyPrior to the Executor API developers were responsible for instantiating and managing threads directly. Lets look at a simple example below.
Simple Thread example
On line 18 the Thread is started by calling its start method. The JVM spawns a new process and executes the run() method in the context of the newly created Thread. Note: you should be careful not to call the run() method directly as this will cause the method to execute in the context of the current thread and will result in single threaded behaviour.
On line 21 we call the join method to block the main thread execution until Thread t1 has terminated. This is only necessary if you want the main thread to wait for the spawned thread to terminate. Often this is not necessary, but for sake of our sample code we want to allow t1 to complete before continuing.
Introducing the Executor ServiceDealing with threads directly is all well and good but Oracle have made things a little easier by providing a layer of abstraction via its Executor API. An Executor allows you to process tasks asynchronously and in parallel without having to deal with threads directly.
Creating an ExecutorThe Executors factory class is used to create an instance of an Executor, either an ExecutorService or a ScheduledExecutorService. Some of the most common types of Executors are described below.
- Executors.newCachedThreadPool() - An ExecutorService with a thread pool that creates threads as required but reuses previously created threads as they become available.
- Executors.newFixedThreadPool(int numThreads) - An ExecutorService that has a thread pool with a fixed number of threads. This is the maximum number of threads that can be active in the ExecutorService at any one time. If the number of requests submitted to the pool exceeds the pool size, requests are queued until a thread becomes available.
- Executors.newScheduledThreadPool(int numThreads) - A ScheduledExecutorService with a thread pool that is used to run tasks periodically or after a specified delay.
- Executors.newSingleThreadExecutor() - An ExecutorService that uses a single thread. Tasks submitted to this ExecutorService will be executed one at a time and in the order submitted.
- Executors.newSingleThreadScheduledExecutor() - An ExecutorService that uses a single thread to execute tasks periodically or after a specified delay.
Below is an example of creating a simple fixed thread pool ExecutorService with a pool size of 2. I'll use this ExecutorService in the following sections.
Creating an ExecutorService using Executor factory method
Using an ExecutorIn the following sections I'm going to look at the various methods available on the ExecutorService for executing tasks asynchronously.
execute(Runnable)The execute method takes a Runnable and is very similar to the simple Thread example we looked at earlier. The execute method is useful when you want to run a task and are not concern about checking its status or obtaining a result. Think of it as a means of invoking a fire and forget asynchronous task.
Execute Runnable with ExecutorService
Future<?> submit(Runnable)The submit method also takes a Runnable but differs from the execute method in that it returns a Future. A Future is an object that represents a pending response form an asynchronous task. Think of it as a handle that can be used to check the status of the task or retrieve its result when the task completes. Futures use generics to allow the user to specify the return type of the task. However, given that a Runnables run method does not return a value (return type void), the Future holds the status of the task rather than a pending result. This is represented as Future<?> as shown in the example below.
Submit Runnable using lambda expression
The submit(Runnable) method is useful when you want to run a task that doesn't return a value but you'd like to check the status of the task after its been submitted to the ExecutorService.
Checking Task StatusFuture provide a few methods that allow you to check the status of a task that's been submitted to the ExecutorService. The isCancelled method checks if a submitted task has already been cancelled. The isDone method allows you to check if the submitted task has completed. isDone will return true regardless of whether a task completed successfully, unsuccessfully or was cancelled. Finally, the cancel method can be used to cancel a submitted task. A boolean parameter indicates whether the task should be interrupted after its already been started.
Future isDone method used to check status of task
Future<T> submit(Callable)The submit method is overloaded to take a Callable as well as a Runnable. A Callable is similar to Runnable in that it represents a task that can be executed on another thread, but differs in that it returns a value and can throw a checked Exception. The Callable interface has the single abstract method public T call() throws Exception and like Runnable can be implemented with an anonymous inner class or functional interface. The return type of the call method is used to type the Future returned by the ExecutorService. There are two examples below, one submitting a Callable using an anonymous inner class and the other using a lambda expression.
Submit Callable using anonymous inner class
Submit Callable using lambda
The example above creates a Callable as a lambda and passes it to the execute method. The Callable will be executed as soon as a thread is available.
Retrieving a Result from a FutureWhen we submit a Callable to the ExecutorService we receive a Future with the return type of the call method. In the example above the call method returns a Double so the Future returned is Future<Double>. One way of retrieving the result from a Future is by calling its get method. This method will block indefinitely waiting on the submitted task to complete. If the task doesn't complete or takes a long time to complete, the main application thread will remain blocked.
Waiting indefinitely for a result is often not ideal. We'd rather have more control over how we retrieve the result and take some action if a task does not complete within a acceptable period of time. Luckily there is an overloaded version of the get method that takes a time value and time unit. This method waits for the specified period of time and if the task is not complete and a result not available, throws a TimeoutException.
Get result from Callable
Submit Multiple CallablesAs well as supporting the submission of a single Callable, the ExecutorService allows you to submit a Collection of Callables using the invokeAll method. As you might expect, instead of receiving a single Future response, a Collection of Futures is returned. A Future is returned representing the pending result of each submitted task.
Invoking a Collection of Callables
Shutting Down the ExecutorServiceAfter all tasks have completed its important to shut down the ExecutorService gracefully so that resources used by the underlying thread pool are reclaimed by the JVM. There are 2 methods available, shutdown and shutdownNow. The shutDown method triggers a shutdown of the ExecutorService, allowing currently processing tasks to finish but rejecting newly submitted tasks.
shutDownNow also triggers a shutdown of the ExecutorService, but does not allow currently executing tasks to complete, attempting to terminate them immediately. shutDownNow returns a list of tasks that were queued for execution when the shutdown was initiated. To avoid potential resource leaks its important that the ExecutorService is shut down inside a finally block.
ExecutorService shut down in finally block