ExecutorService in Java
-
Last Updated: July 15, 2024
-
By: javahandson
-
Series
In this article, we will understand what is an ExecutorService in Java. We will also learn about when to use the executor services, its features, and different methods.
ExecutorService in Java is an interface that provides methods to manage and control thread execution in concurrent Java applications. ExecutorService extends the Executor interface and it gives a more complete, reliable, and extensive feature set for controlling threads in an application that supports concurrency.
As we know threads are used to run the tasks independently or asynchronously. Say we have a main thread and we have a task that we want to run asynchronously then we can make use of a child thread to execute that task. So this task will not be run by the main thread instead it will be run by a child thread.
package com.javahandson; public class Demo { public static void main(String[] args) { // Main thread starts execution Thread childThread = new Thread(new Task()); childThread.start(); // start() method is used to start the execution of the child thread with a new instance of Task // Main thread continues its execution for (int i= 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } } } package com.javahandson; public class Task implements Runnable { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } } } Output: Thread-0:0 main:0 Thread-0:1 Thread-0:2 main:1 main:2 main:3 main:4 Thread-0:3 Thread-0:4
From the above output, we can see that the main thread and child thread are running independently of each other and we can see some scattered output.
Now if we have to run 5 tasks instead of 1 then we can use a for loop to create 5 child threads and run them asynchronously.
package com.javahandson; public class Demo { public static void main(String[] args) { // Main thread starts execution for (int i = 0; i < 5; i++) { // As this loop executes 5 times hence 5 threads will be created Thread childThread = new Thread(new Task()); childThread.start(); // start() method is used to start the execution of the child thread with a new instance of Task } // Main thread continues its execution for (int i= 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } } } Output: Thread-4:0 Thread-3:0 Thread-3:1 Thread-3:2 Thread-1:0 Thread-3:3 Thread-0:0 Thread-4:1 Thread-0:1 Thread-0:2 Thread-0:3 Thread-0:4 main:0 main:1 main:2 Thread-3:4 Thread-1:1 Thread-2:0 Thread-1:2 Thread-4:2 Thread-1:3 Thread-2:1 Thread-1:4 Thread-4:3 main:3 main:4 Thread-2:2 Thread-2:3 Thread-4:4 Thread-2:4
Now what happens if we have to run 500 tasks asynchronously? According to the above logic, we will create 500 threads using the for-loop.
for (int i = 0; i < 5; i++) { // As this loop executes 500 times hence 5 threads will be created Thread childThread = new Thread(new Task()); childThread.start(); }
But it is not a good way because
1 Java thread = 1 OS thread
If we run the above code 500 OS threads will be created and 1 OS thread runs on 1 core of the CPU. High-end systems will have at most of 24-32 cores so if we have to run 500 threads then we will need 500 cores or if we have 24 cores then the possibility is that 24 threads will be running asynchronously at a time and the other threads will be in waiting state. Creating threads is a very costly operation and creating 500 threads is a very very bad option. We should never do this as it will be an overload on the Operating system.
So instead of creating 500 threads, we will create a fixed number of threads i.e. 10 and we will submit 500 tasks to those 10 threads. Now thread-1 will pick a task-1 and start execution meanwhile other threads are also executing other tasks asynchronously. So once thread-1 completes task-1 it will pick task-10 and start working on it ( as the other 10 threads are already working on task-2 to task-9 tasks ). So in this way to be precise thread-1 will perform 50 tasks, thread-2 will perform 50 tasks, and so on. In this way, 10 threads will combine to complete 500 tasks.
So this is possible using ExecutorService in Java.
Thread Pool Management: ExecutorService helps us to create a pool of threads and manage them. Using a thread pool we can create a predefined number of threads and we will only use those threads to execute the tasks. The same threads will be reused again to execute other tasks till all the tasks are completed.
Task Submissions: ExecutorService has a set of methods like submit(), invokeAll(), and invokeAny() to submit tasks for execution. The submitted tasks can be of Runnable or Callable type. Runnable tasks are the tasks that don’t return a value whereas Callable tasks can return a value.
Shutdown service: ExecutorService provides shutdown() and shutdownNow() methods to stop the executor service gracefully or abruptly. shutdown method will make sure to complete all the pending tasks and it will not accept any new task whereas the shutdownNow method will attempt to terminate all the active executing tasks and return a list of the tasks that were awaiting execution.
Future results: Tasks submitted to the ExecutorService can return Future objects. These Future objects are used to check the status and results of an execution like if the task is completed or whether it’s been cancelled etc.
ExecutorService can be instantiated in multiple ways. Below are a few of them.
The java.util.concurrent.Executors class provides factory and utility methods for Executor, ExecutorService, ScheduledExecutorService, ThreadFactory, and Callable classes in Java. It is often used to create an instance of ExecutorService.
Here’s how you can create an ExecutorService instance:
ExecutorService executorService = Executors.newFixedThreadPool(10);
newFixedThreadPool(10) creates an ExecutorService that reuses a fixed number of threads. Here, 10 is the fixed number of threads that are already created. If at any point all threads are active and additional tasks are submitted, they will wait in the queue until a thread is available.
ExecutorService executorService = Executors.newSingleThreadExecutor();
newSingleThreadExecutor() uses a single worker thread. Here since we have only one thread so we cannot run multiple tasks asynchronously. Hence it is ideal for tasks that need to be executed sequentially in the order they are added.
ExecutorService executorService = Executors.newCachedThreadPool();
newCachedThreadPool() will create a new cached thread pool and this thread pool will create the new thread when needed but will reuse previously constructed threads when they are available. These pools are typically useful for executing many short-lived asynchronous tasks.
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);
newScheduledThreadPool(5) will create a new scheduled thread pool with a fixed number of threads. As the name says scheduled will help to run tasks after a given delay.
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5); // This task is to print "Hello World" after a delay of 5 seconds Runnable task = () -> System.out.println("Hello World"); executorService.schedule(task, 5, TimeUnit.SECONDS);
If you want to know more about the Executors class please refer to this Oracle document https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executors.html
ThreadPoolExecutor is a concrete class in the java.util.concurrent package that provides an implementation of the ExecutorService interface. ThreadPoolExecutor is used to create and manage a pool of threads for executing tasks concurrently. ThreadPoolExecutor has a set of worker threads and a queue. When a task is submitted it is added to a queue. The threads in the pool take the tasks from the queue and execute them.
ExecutorService executorService = new ThreadPoolExecutor(corePoolSize,
maximumPoolSize, keepAliveTime, TimeUnit.SECONDS,
new LinkedBlockingQueue<>());
If you want to understand more about ThreadPoolExecutor you can refer to this Oracle document https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ThreadPoolExecutor.html
The runnable interface has a run method that does not return any value. We can run a runnable task using an execute() as well as submit(). The difference between execute() and submit() is that execute() doesn’t return a result. On the other hand, submit() returns a Future object, which can be used to check the execution status and to cancel the execution.
Write a program to execute runnable tasks using the execute method.
package com.javahandson; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Demo { public static void main(String[] args) { // creating ExecutorService with fixed thread pool of size 2 ExecutorService executorService = Executors.newFixedThreadPool(2); // creating a Runnable task Runnable runnableTask1 = () -> { for (int i= 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } }; // creating a Runnable task Runnable runnableTask2 = () -> { for (int i= 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } }; // executing the task using executorService executorService.execute(runnableTask1); executorService.execute(runnableTask2); // shutting down the executorService executorService.shutdown(); } } Output: pool-1-thread-2:0 pool-1-thread-2:1 pool-1-thread-2:2 pool-1-thread-2:3 pool-1-thread-1:0 pool-1-thread-1:1 pool-1-thread-1:2 pool-1-thread-1:3 pool-1-thread-1:4 pool-1-thread-2:4
Write a program to execute runnable tasks using the submit method.
package com.javahandson; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Demo { public static void main(String[] args) throws InterruptedException, ExecutionException { // creating ExecutorService with fixed thread pool of size 2 ExecutorService executorService = Executors.newFixedThreadPool(2); // creating a Runnable task Runnable runnableTask1 = () -> { for (int i= 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } }; // creating a Runnable task Runnable runnableTask2 = () -> { for (int i= 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } }; // submitting the tasks to executorService Future<?> future1 = executorService.submit(runnableTask1); Future<?> future2 = executorService.submit(runnableTask2); boolean isTask1Done = future1.isDone(); System.out.println("Task1 has finished executing : "+ isTask1Done); boolean isTask2Done = future2.isDone(); System.out.println("Task2 has finished executing : "+ isTask2Done); // Checking if the task has finished executing. // This will block until the task has finished executing. future1.get(); future2.get(); isTask1Done = future1.isDone(); System.out.println("Task1 has finished executing : "+ isTask1Done); isTask2Done = future2.isDone(); System.out.println("Task2 has finished executing : "+ isTask2Done); // shutting down the executorService executorService.shutdown(); } } Output: Task1 has finished executing : false Task2 has finished executing : false pool-1-thread-1:0 pool-1-thread-1:1 pool-1-thread-1:2 pool-1-thread-1:3 pool-1-thread-1:4 pool-1-thread-2:0 pool-1-thread-2:1 pool-1-thread-2:2 pool-1-thread-2:3 pool-1-thread-2:4 Task1 has finished executing : true Task2 has finished executing : true
The ExecutorService doesn’t offer a built-in method to submit multiple Runnable tasks at once, but you can achieve this using a loop to submit each task individually. Here’s a sample:
// creating a list of tasks List<Runnable> tasks = Arrays.asList(runnableTask1, runnableTask2, runnableTask3); // submitting the tasks to executorService all at once for (Runnable task : tasks) { executorService.submit(task); }
The callable interface has a call method that returns a value. We can run a callable task using a submit(). The submit() returns a Future object, which can be used to check the execution status and cancel the execution. As we are running a callable task hence the future object will contain the resultant value of the call method as well.
Write a program to execute a callable task.
package com.javahandson; import java.util.concurrent.*; public class Demo { public static void main(String[] args) throws InterruptedException, ExecutionException { // creating ExecutorService with fixed thread pool of size 1 ExecutorService executorService = Executors.newFixedThreadPool(1); // creating a Callable task Callable<Integer> task = () -> { int sum = 0; for (int i = 0; i < 10; i++) { sum += i; } return sum; }; // submitting the task to executorService Future<Integer> future = executorService.submit(task); boolean isTaskDone = future.isDone(); System.out.println("Task has finished executing : "+ isTaskDone); // printing the result of the task System.out.println("Sum: " + future.get()); isTaskDone = future.isDone(); System.out.println("Task has finished executing : "+ isTaskDone); // shutting down the executorService executorService.shutdown(); } } Output: Task has finished executing : false Sum: 45 Task has finished executing : true
The invokeAll() method executes all the given collection of Callable tasks.
<T> List<Future<T>> invokeAll(Collectio<? extends Callable<T>> tasks)
T: It is a type parameter. It indicates that this method can operate on any type. The type T that we specify will be the return type of Callable tasks and the Future objects.
List<Future<T>>: This is the return type of the method. It returns a list of Future objects that represent the result of the tasks performed by the threads. This list will contain the results in the same sequential order.
invokeAll(Collection<? extends Callable<T>> tasks): The method takes a Collection of Callable tasks as input. The ? extends Callable<T> is a bounded wildcard. It means that the collection can be of Callable<T> or any subtype of Callable<T>. Each Callable returns an object of type T or a subtype of T.
When we call invokeAll(), it executes all the Callable tasks in the executor’s thread pool concurrently. The method blocks until all tasks have completed execution. Then, it returns a list of Future objects.
package com.javahandson; import java.util.Arrays; import java.util.List; import java.util.concurrent.*; public class Demo { public static void main(String[] args) throws InterruptedException, ExecutionException { // creating ExecutorService with fixed thread pool of size 1 ExecutorService executorService = Executors.newFixedThreadPool(2); // creating a Callable task Callable<Integer> task1 = () -> { int sum = 0; for (int i = 0; i < 10; i++) { sum += i; } return sum; }; Callable<Integer> task2 = () -> { int sum = 0; for (int i = 10; i < 20; i++) { sum += i; } return sum; }; List<Callable<Integer>> tasks = Arrays.asList(task1, task2); List<Future<Integer>> futures = executorService.invokeAll(tasks); int counter = 1; for (Future<Integer> future : futures) { System.out.println("Sum of task executed by thread "+ counter +" : "+ future.get()); counter++; } // shutting down the executorService executorService.shutdown(); } } Output: Sum of task executed by thread 1 : 45 Sum of task executed by thread 2 : 145
The invokeAny() method executes a collection of Callable tasks and returns the result of any one task that has been completed successfully (without throwing an exception). This method executes all the tasks asynchronously. This method blocks until at least one of the tasks has been completed and returns the result of that task.
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
T: It is a type parameter. It indicates that this method can operate on any type. The type T that we specify will be the return type of Callable tasks and the Future objects.
T: This is the return type of the method. The method returns the result of any one of the Callable tasks that are completed.
invokeAny(Collection<? extends Callable<T>> tasks): This method takes a Collection of Callable tasks as input. The ? extends Callable<T> is a bounded wildcard. It means that the collection can be of Callable<T> or any subtype of Callable<T>. Each Callable returns an object of type T or a subtype of T.
package com.javahandson; import java.util.Arrays; import java.util.List; import java.util.concurrent.*; public class Demo { public static void main(String[] args) throws InterruptedException, ExecutionException { // creating ExecutorService with fixed thread pool of size 1 ExecutorService executorService = Executors.newFixedThreadPool(2); // creating a Callable task Callable<Integer> task1 = () -> { int sum = 0; for (int i = 0; i < 10; i++) { sum += i; } return sum; }; Callable<Integer> task2 = () -> { int sum = 0; for (int i = 10; i < 20; i++) { sum += i; } return sum; }; List<Callable<Integer>> tasks = Arrays.asList(task1, task2); Integer result = executorService.invokeAny(tasks); System.out.println("Sum of task : "+ result); // shutting down the executorService executorService.shutdown(); } } Output: Sum of task : 45
In the above program, we have submitted 2 callable tasks to the ExecutorService and it has returned the result of one of the tasks.
This method checks if the executor has been shut down or not.
boolean isShutdown()
boolean: It is a return type that returns true or false. If the executor is shut down then it will return true else false.
package com.javahandson; import java.util.concurrent.*; public class Demo { public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(1); boolean isShutDown = executorService.isShutdown(); System.out.println("Executor has been shut down : " + isShutDown); executorService.shutdown(); // shutdown the executor isShutDown = executorService.isShutdown(); System.out.println("Executor has been shut down : " + isShutDown); } } Output: Executor has been shut down : false Executor has been shut down : true
This method checks if all tasks have been completed following shutdown.
boolean isTerminated()
boolean: It is a return type that returns true or false. If all the tasks have been completed following shutdown then it returns true else false.
package com.javahandson; import java.util.Arrays; import java.util.List; import java.util.concurrent.*; public class Demo { public static void main(String[] args) throws InterruptedException, ExecutionException { // creating ExecutorService with fixed thread pool of size 1 ExecutorService executorService = Executors.newFixedThreadPool(2); // creating a Callable task Callable<Integer> task1 = () -> { int sum = 0; for (int i = 0; i < 10; i++) { sum += i; } return sum; }; Callable<Integer> task2 = () -> { int sum = 0; for (int i = 10; i < 20; i++) { sum += i; } return sum; }; List<Callable<Integer>> tasks = Arrays.asList(task1, task2); List<Future<Integer>> futures = executorService.invokeAll(tasks); for (Future<Integer> future : futures) { System.out.println("Sum of task : "+ future.get()); } // shutting down the executorService executorService.shutdown(); while (!executorService.isTerminated()) { System.out.println("Waiting for all tasks to complete..."); Thread.sleep(1000); } boolean allTasksCompleted = executorService.isTerminated(); System.out.println("All tasks completed : " + allTasksCompleted); } } Output: Sum of task : 45 Sum of task : 145 Waiting for all tasks to complete... All tasks completed : true
This method initiates an orderly shutdown in which previously submitted tasks will be executed, but no new tasks will be accepted. This method does not return anything.
This method is used to perform the following steps:
Stop all actively executing tasks: The method tries to cancel as many tasks that are currently running as it can. However, it provides no guarantees beyond that and it can’t stop any tasks that aren’t responsive to interrupts.
Halt the processing of waiting tasks: Any tasks that were submitted to the ExecutorService but not yet started will be removed from the task queue and won’t be started.
Returns a list of the tasks that were awaiting execution: The method returns a List that contains the tasks that were in the queue waiting to be executed. These are tasks that were submitted to the ExecutorService but were not yet started.
List<Runnable> shutdownNow()
List<Runnable>: These are the tasks that were submitted for execution but might never start if shutdownNow is called before it gets a chance to run.
package com.javahandson; import java.util.List; import java.util.concurrent.*; public class Demo { public static void main(String[] args) { ExecutorService executorService = Executors.newSingleThreadExecutor(); executorService.submit(() -> { try { System.out.println("Running task 1..."); Thread.sleep(10000); // simulate a long running task System.out.println("Completed task 1"); } catch (InterruptedException e) { System.out.println("Task 1 was interrupted"); } }); executorService.submit(() -> { System.out.println("This is task 2, which might never start if shutdownNow " + "is called before it gets a chance to run."); }); List<Runnable> neverStartedTasks = executorService.shutdownNow(); System.out.println("Number of tasks never started: " + neverStartedTasks.size()); } } Output: Running task 1... Task 1 was interrupted Number of tasks never started: 1
submit returns a Future object, which can be used to check the execution status and cancel the execution. In the ExecutorService interface submit method is overridden in 3 ways:
<T> Future<T> submit(Callable<T> task)
It submits a value-returning task or the callable task for execution and returns a Future representing the pending results of the task. The Future’s get method will return the task’s result upon successful completion.
Future<?> submit(Runnable task)
It submits a Runnable task for execution and returns a Future representing that task. The Future’s get method will return a null value upon successful completion.
We have seen examples of both these methods in the above sections. The third one is:
<T> Future<T> submit(Runnable task, T result)
It submits a Runnable task for execution and returns a Future representing that task. The Future’s get method will return the given result upon successful completion of the task.
As we know Runnable task does not return anything hence as part of this method we are enforcing to return of some output when the task is completed successfully.
package com.javahandson; import java.util.concurrent.*; public class Demo { public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService executorService = Executors.newSingleThreadExecutor(); Runnable runnableTask = () -> { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } }; String returnValue = "Task has finished successfully"; Future<String> future = executorService.submit(runnableTask, returnValue); String result = future.get(); System.out.println(result); executorService.shutdown(); } } Output: pool-1-thread-1:0 pool-1-thread-1:1 pool-1-thread-1:2 pool-1-thread-1:3 pool-1-thread-1:4 Task has finished successfully
In summary, ExecutorService in Java provides a high-level replacement for working with threads directly for concurrent programming, providing several utility methods to control and manage thread execution.
So this is all about ExecutorService in Java. If you have any questions on this topic, please raise them in the comments section. If you liked this article then please share this post with your friends and colleagues.