Learn to use Java ExecutorService to execute a Runnable
or Callable
class in an asynchronous way. Also learn the various best practices to utilize it in the most efficient manner in any Java application.
1. What is Executor Framework?
In simple Java applications, we do not face many challenges while working with a small number of threads. If we have to develop a program that runs a lot of concurrent tasks, this approach will present many disadvantages such as lots of boilerplate code (create and manage threads), executing threads manually and keeping track of thread execution results.
Executor framework (since Java 1.5) solved this problem. The framework consists of three main interfaces (and lots of child interfaces):
- Executor,
- ExecutorService
- ThreadPoolExecutor
1.1. Benefits of Executor Framework
- The framework mainly separates task creation and execution. Task creation is mainly boilerplate code and is easily replaceable.
- With an executor, we have to create tasks that implement either Runnable or Callable interface and send them to the executor.
- Executor internally maintains a (configurable) thread pool to improve application performance by avoiding the continuous spawning of threads.
- Executor is responsible for executing the tasks, and running them with the necessary threads from the pool.
1.2. Callable and Future
Another important advantage of the Executor framework is the use of the Callable interface. It’s similar to the Runnable
interface with two benefits:
- It’s
call()
method returns a result after the thread execution is complete. - When we send a
Callable
object to an executor, we get aFuture
object’s reference. We can use this object to query the status of the thread and the result of theCallable
object.
2. Creating ExecutorService Instance
ExecutorService
is an interface and its implementations can execute a Runnable
or Callable
class in an asynchronous way. Note that invoking the run()
method of a Runnable
interface in a synchronous way is simply calling a method.
We can create an instance of ExecutorService
in following ways:
2.1. Using Executors
Executors
is a utility class that provides factory methods for creating the implementations of the interface.
//Executes only one thread
ExecutorService es = Executors.newSingleThreadExecutor();
//Internally manages thread pool of 2 threads
ExecutorService es = Executors.newFixedThreadPool(2);
//Internally manages thread pool of 10 threads to run scheduled tasks
ExecutorService es = Executors.newScheduledThreadPool(10);
2.2. Using Constructors
We can choose an implementation class of ExecutorService
interface and create its instance directly. The below statement creates a thread pool executor with a minimum thread count 10, maximum threads count 100 and 5 milliseconds keep alive time and a blocking queue to watch for tasks in future.
ExecutorService executorService = new ThreadPoolExecutor(10, 100, 5L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
3. Submitting Tasks to ExecutorService
Generally, tasks are created by implementing either Runnable
or Callable
interface. Let’s see the example of both cases.
3.1. Executing Runnable Tasks
We can execute runnables using the following methods :
- void execute(Runnable task) – executes the given command at some time in the future.
- Future submit(Runnable task) – submits a runnable task for execution and returns a
Future
representing that task. The Future’sget()
method will returnnull
upon successful completion. - Future submit(Runnable task, T result) – Submits a runnable task for execution and returns a
Future
representing that task. The Future’sget()
method will return the givenresult
upon successful completion.
In given example, we are executing a task of type Runnable
using both methods.
import java.time.LocalDateTime;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class Main
{
public static void main(String[] args)
{
//Demo task
Runnable runnableTask = () -> {
try {
TimeUnit.MILLISECONDS.sleep(1000);
System.out.println("Current time :: " + LocalDateTime.now());
} catch (InterruptedException e) {
e.printStackTrace();
}
};
//Executor service instance
ExecutorService executor = Executors.newFixedThreadPool(10);
//1. execute task using execute() method
executor.execute(runnableTask);
//2. execute task using submit() method
Future<String> result = executor.submit(runnableTask, "DONE");
while(result.isDone() == false)
{
try
{
System.out.println("The method return value : " + result.get());
break;
}
catch (InterruptedException | ExecutionException e)
{
e.printStackTrace();
}
//Sleep for 1 second
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//Shut down the executor service
executor.shutdownNow();
}
}
Program output.
Current time :: 2019-05-21T17:52:53.274
Current time :: 2019-05-21T17:52:53.274
The method return value : DONE
3.2. Execute Callable Tasks
We can execute callable tasks using the following methods :
- Future submit(callableTask) – submits a value-returning task for execution and returns a Future representing the pending results of the task.
- List<Future> invokeAll(Collection tasks) – executes the given tasks, returning a list of Futures holding their status and results when all complete. Notice that result is available only when all tasks are completed.
Note that a completed task could have terminated either normally or by throwing an exception. - List<Future> invokeAll(Collection tasks, timeOut, timeUnit) – executes the given tasks, returning a list of Futures holding their status and results when all complete or the timeout expires.
In given example, we are executing a task of type Callable
using both methods.
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class Main
{
public static void main(String[] args) throws ExecutionException
{
//Demo Callable task
Callable<String> callableTask = () -> {
TimeUnit.MILLISECONDS.sleep(1000);
return "Current time :: " + LocalDateTime.now();
};
//Executor service instance
ExecutorService executor = Executors.newFixedThreadPool(1);
List<Callable<String>> tasksList = Arrays.asList(callableTask, callableTask, callableTask);
//1. execute tasks list using invokeAll() method
try
{
List<Future<String>> results = executor.invokeAll(tasksList);
for(Future<String> result : results) {
System.out.println(result.get());
}
}
catch (InterruptedException e1)
{
e1.printStackTrace();
}
//2. execute individual tasks using submit() method
Future<String> result = executor.submit(callableTask);
while(result.isDone() == false)
{
try
{
System.out.println("The method return value : " + result.get());
break;
}
catch (InterruptedException | ExecutionException e)
{
e.printStackTrace();
}
//Sleep for 1 second
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//Shut down the executor service
executor.shutdownNow();
}
}
Program output.
Current time :: 2019-05-21T18:35:53.512
Current time :: 2019-05-21T18:35:54.513
Current time :: 2019-05-21T18:35:55.514
The method return value : Current time :: 2019-05-21T18:35:56.515
Notice that tasks have been completed with a delay of 1 second because there is only one task in the thread pool. But when you run the program, all first 3 print statements appear at the same time because even if the tasks are complete, they wait for other tasks to complete in the list.
4. How to Shutdown ExecutorService
The final and most important thing that many developers miss is shutting down the ExecutorService
. The ExecutorService
is created and it has Thread
elements.
Remember that the JVM stops only when all non-daemon threads are stopped. Here not shutting down the executor service simply prevents the JVM from stopping.
In the above examples, if we comment out the executor.shutdownNow() method call, then even after all tasks are executed, main thread remains active and JVM does not stop.
To tell the executor service that there is no need for the threads it has, we will have to shutdown the service.
There are three methods to invoke shutdown:
- void shutdown() – Initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted.
- List<Runnable> shutdownNow() – Attempts to stop all actively executing tasks, halts the processing of waiting tasks, and returns a list of the tasks that were awaiting execution.
- void awaitTermination() – It blocks until all tasks have completed execution after a shutdown request, or the timeout occurs, or the current thread is interrupted, whichever happens first.
Use any of the above 3 methods wisely as per the requirements of the application.
5. Best Practices
- Always run your Java code against static analysis tools like PMD and FindBugs to look for deeper issues. They are very helpful in determining ugly situations which may arise in the future.
- Always cross-check and better plan a code review with senior guys to detect possible deadlock or livelock in code during execution. Adding a health monitor to your application to check the status of running tasks is an excellent choice in most scenarios.
- In multi-threaded programs, make a habit of catching errors too, not just exceptions. Sometimes unexpected things happen, and Java throws an error at you, apart from an exception.
- Use a back-off switch, so if something goes wrong and is non-recoverable, you don’t escalate the situation by eagerly starting another loop. Instead, you need to wait until the situation goes back to normal and then start again.
- Please note that the whole point of executors is to abstract away the specifics of execution, so ordering is not guaranteed unless explicitly stated.
6. Conclusion
As discussed above, ExecutorService
helps in minimizing the boilerplate code which is a good thing. It also helps in better resource management by internally utilizing a thread pool.
Still, programmers should be careful to avoid some common mistakes. E.g. always shut down the executor service after tasks are completed and service is no longer needed. Otherwise, JVM will never terminate, normally.
Similarly, while creating it’s instance, be mindful of the configured thread pool capacity. Here or in any other implementation, a careless threads pool size can halt the system and bring performance down.
And finally, make a practice of using timeout parameters in blocking method calls. These methods can block the whole application execution if not returned in small time.
7. More Examples
- ExecutorService invokeAny() – Run multiple tasks and process first result
- ExecutorService invokeAll() – Run multiple tasks and process all results
- ExecutorService – shutdown(), shutdownNow() and awaitTermination() APIs
- ScheduledExecutorService – Run task in executor after delay
- ExecutorService – Cancel a task in Executor
- ExecutorService RejectedExecutionHandler – Rejected task handling example
- ExecutorService – Callable Future Example
Happy Learning !!
Comments