Java ExecutorService
Executor framework was introduced in java 5 to simplify the execution of tasks in different threads.
With executor framework, you can define a task that needs to be executed in a different thread and provide that task to objects that belong to the framework. It will take care of executing that task.
In this article we will take a deep look into Java executor framework.
Executor framework comprises of a set of classes and interfaces, responsible for accepting a task, executing it and returning the result, if required.
Following are the important and most frequently used classes and interfaces in java executor framework.
1. Executor
This is the core interface which executes tasks submitted to it. It has a single method execute()
which accepts a Runnable
object as argument.
Remember that a class that implements Runnable interface is executed in a new thread in java.
2. ExecutorService
Child interface of Executor
providing additional features such as
- tracking of tasks to determine whether they are completed or not,
- cancel execution of tasks,
- wait for completion of tasks,
- bulk execution of collection of tasks,
- shutting down executor service so that no new tasks are accepted etc.
3. ScheduledExecutorService
Extends ExecutorService
and provides support to run tasks after a delay or repeat them after a certain interval.
Contains methods such as schedule()
, scheduleAtFixedRate()
etc.
4. ThreadPoolExecutor
Implementation of ExecutorService
providing thread pool functionality for efficiently running multiple tasks.
5. ForkJoinPool
Another implementation of ExecutorService
which executes tasks by breaking them into smaller tasks and provides result by combining the results of sub-tasks, analogous to Divide and Conquer algorithm.
6. Executors
This is a utility class which contains methods to create objects of ExecutorService
, ScheduledExecutorService
etc., that are used for executing tasks.
It is also possible to create these objects directly but it is a bit difficult as we will see in a few moments.
There are many other classes and interfaces that belong to executor framework but not frequently used.
Two other interfaces that will be discussed later are Future
and Callable
.
Advantages of executor framework
1. Separates task execution with its submission.
2. No need to explicitly call start()
method to execute a new thread.
3. Provides built in connection pooling.
4. Support for returning a value from a task executed on new thread.
Note that with traditional java thread, there is no mechanism to get a value from thread.
5. Provision to cancel a task, which may or may not have started.
6. Allows asynchronous execution of tasks, meaning that the current executing thread can run in non-blocking manner.
Create ExecutorService
There are two ways to create an object of ExecutorService
class, which is primarily responsible for task execution.
1. Constructors
ExecutorService
is an interface.
So, to create its object, we need to create an object of a class that implements it, such as ThreadPoolExecutor
.
It has many overloaded constructors, the simplest one being
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
We can create its object using this constructor as shown below
ExecutorService e = new ThreadPoolExecutor(5, 10, 50, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
As you can see, it seems a bit complicated.
To simplify, use the second method explained below.
2. Using Executors
As stated earlier, Executors
is a utility class for creating objects of ExecutorService
.
It has static methods for the same. Example,
// create a single thread executor ExecutorService single = Executors.newSingleThreadExecutor(); // create thread pool executor ExecutorService pooled = Executors.newFixedThreadPool(10); // create scheduled single thread executor ExecutorService sched = newSingleThreadScheduledExecutor(); // create scheduled thread pool executor ExecutorService pSched = newScheduledThreadPool(10);
ExecutorService example
Below is a class that implements Runnable
interface and overrides its run()
method.
An object of this class represents a task executed on a new thread.
class Task implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }
This tasks only prints the name of the thread which executes it.
Now, to run this task using executor service in java, first create an object of ExecutorService
, then call execute()
method providing it the task to be executed as shown below
// create object of executor service ExecutorService e = Executors.newSingleThreadExecutor(); System.out.println("Thread submitting task: " + Thread.currentThread().getName()); // execute task e.execute(new Task());
Below is the output
Thread submitting task: main
pool-1-thread-1
It shows that the task is executed by a different thread than the thread which submitted it.
Thread pool
Above example created a single thread for execution. This means that if multiple tasks are submitted, only one will be executed at a time, others will be in waiting state.
To show this, add a delay in the task that will be executed on a new thread , to simulate a time tasking operation as shown below.
class Task implements Runnable { @Override public void run() { try { System.out.println("Task execution started"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()); System.out.println("------ Task completed ------"); } }
Now, create a single thread executor and submit multiple tasks in a loop as shown below
ExecutorService e = Executors.newSingleThreadExecutor(); System.out.println("Thread submitting task: " + Thread.currentThread().getName()); for (int i = 0; i < 3; i++) { e.execute(new Task()); }
Below is the output
Thread submitting task: main
Task execution started
pool-1-thread-1
—— Task completed ——
Task execution started
pool-1-thread-1
—— Task completed ——
Task execution started
pool-1-thread-1
—— Task completed ——
You will notice that once a task is completed, only then a new task is picked up for execution.
Also, the name of the thread executing all the tasks is the same, which shows that only one worker thread is active.
To enable multiple tasks being executed concurrently, we need a thread pool.
Java executor service provides an option to create a thread pool using newFixedThreadPool()
method from Executors
class.
This method takes an integer argument representing the number of threads that will be present in the pool.
Above code modified to use a thread pool is shown below
ExecutorService e = Executors.newFixedThreadPool(5); System.out.println("Thread submitting task: " + Thread.currentThread().getName()); for (int i = 0; i < 3; i++) { e.execute(new Task()); }
Output is
Thread submitting task: main
Task execution started
Task execution started
Task execution started
pool-1-thread-1
—— Task completed ——
pool-1-thread-2
—— Task completed ——
pool-1-thread-3
—— Task completed ——
Notice that all tasks are being executed simultaneously and the name of thread executing them is also different.
This example creates a pool of 5 threads. This means that at a time, 5 tasks can be executed simultaneously.
As soon as a thread completes execution, it goes back to the pool waiting for the next task.
If a task arrives at the time all threads are busy executing other tasks, then it is added to a queue till a thread becomes free.
Task as Lambda expression
In above examples, we saw that each task is an object of a class that implements Runnable
interface.
This means that to create a task,
1. We need to define a class which implements Runnable
, and
2. Implement run()
method in that class.
With java 8 Lambda expressions, these steps are not required anymore.
Since Runnable
is a Functional interface, we can implement its run()
method using Lambda expression.
So, above code can be modified as
ExecutorService e = Executors.newFixedThreadPool(5); System.out.println("Thread submitting task: " + Thread.currentThread().getName()); Runnable task = () -> System.out.println(Thread.currentThread().getName()); for (int i = 0; i < 3; i++) { e.execute(task); }
With this, we do not need to create Task
class.
Get return value
Till now, we learnt that tasks submitted to executor service need to implement run()
method of Runnable
interface.
Return type of run()
is void
. This means that it can not return a value.
But, what if we want a task executed in a separate thread return a value.
This is possible with submit()
method of executor service. This method accepts an object implementing Callable
interface.Callable
interface is similar to Runnable
as it also has a single method call()
. But, call()
returns a value.
submit()
returns a value of type Future
, which is an interface. To retrieve the value returned from thread execution, use its get()
method.
To demonstrate the usage of Future, create a new class that implements Callable
. Objects of this class are the tasks executed on a separate thread.
class ReturnTask implements Callable<Integer> { @Override public Integer call() throws Exception { return 1; } }
Note that Callable
is a generic type and the return type of call()
is the same as the type of this class.
Next, create an executor service with a single thread or a thread pool and call its submit()
method, with an object of ReturnTask
.
Finally, invoke get()
method to retrieve the value returned by call()
as shown
ExecutorService e = Executors.newSingleThreadExecutor(); Future<Integer> future = e.submit(new ReturnTask()); System.out.println(future.get()); // 1
Few important points to note are
- The task is executed as soon as
submit()
is called. - Task defined in
submit()
executes on a different thread other than calling thread. get()
method blocks the execution of current thread.
This means that if the task defined insubmit()
takes time andget()
is called before it completes, then the thread callingget()
will keep on waiting tillsubmit()
finishes and returns a value.- For that reason, there is an overloaded
get()
method, which accepts a timeout value, so that the calling thread does not keep waiting forever.
call()
method can also be implemented as a Lambda expression, since Callable
is a functional interface as shown below
ExecutorService e = Executors.newSingleThreadExecutor(); Future<Integer> future = e.submit( () -> 1 ); System.out.println(future.get()); // 1
There is no need to define a separate class that implements Callable
.
Shutting down executor
If you execute a program using an executor service, you will notice that the program does not terminate on its own.
This is because, the executor service keeps on running and waiting for new tasks forever.
To close an executor service, it provides two methods
1. shutdown()
Stops the executor service so that no new tasks are accepted.
Currently running or already submitted tasks are executed. Example,
ExecutorService e = Executors.newSingleThreadExecutor(); Runnable r = () -> { try { Thread.sleep(5000); System.out.println("Task completed"); } catch (InterruptedException e1) { e1.printStackTrace(); } }; e.execute(r); System.out.println("shutting down"); e.shutdown(); System.out.println("shut down");
Output is
shutting down
shut down
Task completed
This shows that even after shutdown()
is called, tasks previously submitted are executed.
If any new tasks are submitted with execute()
or submit()
after calling shutdown()
, java.util.concurrent.RejectedExecutionException is raised.
2. shutdownNow()
Similar to shutdown()
except that, it tries to stop the currently executing tasks that were submitted with the executor service’s execute()
or submit()
methods.
Internally, it invokes Thread.interrupt()
method of the task threads. Example,
ExecutorService e = Executors.newSingleThreadExecutor(); Runnable r = () -> { try { Thread.sleep(5000); } catch (InterruptedException e1) { e1.printStackTrace(); } }; e.execute(r); System.out.println("shutting down"); e.shutdownNow();
Task submitted to executor service takes 5 seconds to complete but before its completion, shutdownNow()
is called.
Below is the output
shutting down
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
It shows that the the executor service tries to stop the executing tasks.
shutdownNow()
returns a list of tasks that were submitted but have not yet started execution.
Waiting for termination
Both shutdown()
and shutdownNow()
methods do not wait for the executing tasks to finish.
It means that the code after these methods is executed immediately after these are called, irrespective of the tasks submitted. Example,
ExecutorService e = Executors.newSingleThreadExecutor(); Runnable r = () -> { try { Thread.sleep(5000); System.out.println("Task completed"); } catch (InterruptedException e1) { e1.printStackTrace(); } }; e.execute(r); System.out.println("shutting down"); e.shutdown(); System.out.println("shut down");
Look at the output
shutting down
shut down
Task completed
The statement after shutdown()
is executed immediately.
If you want the execution to wait for the submitted tasks to finish, use awaitTermination()
.
It takes the time to wait and its unit as arguments. Example,
ExecutorService e = Executors.newSingleThreadExecutor(); Runnable r = () -> { Thread.sleep(5000); System.out.println("Task completed"); }; e.execute(r); System.out.println("shutting down"); e.shutdown(); try { e.awaitTermination(6, TimeUnit.SECONDS); } catch (InterruptedException e1) { e1.printStackTrace(); } System.out.println("shut down");
Output is
shutting down
Task completed
shut down
This shows that the thread in which executor service is running waits for the tasks to complete.
Note that awaitTermination()
blocks the current thread for the time taken by the tasks to complete or argument supplied to it, whichever is smaller.
Remember that it is necessary to call shutdown()
or shutdownNow()
before awaitTermination()
, otherwise the current thread will remain in waiting state forever.
Hope the article was useful.