ExecutorService in Java – Java ExecutorService Examples

In this TechVidvan Java tutorial, we will learn about the executorservice in Java. We already know that Java works very efficiently with multithreaded applications that require to execute the tasks concurrently in a thread.

It is challenging for any application to execute a large number of threads simultaneously. Thus, to overcome this problem, Java provides the ExecutorService interface, which is a sub-interface of the Executors framework.

In this article, we will understand how to create an ExecutorService. And, how to submit tasks for execution to executor service, We also discuss how we can see the results of those tasks.

At last, we will study how to shut down the ExecutorService again when required.

Executor Service in java

What is the Executor Framework?

It is easier for us to create and execute one or two threads simultaneously. But when the number of threads increases to a significant number, it becomes difficult. Many multi-threaded applications have hundreds of threads running simultaneously.

Therefore, there is a need to separate the creation of the thread from the management of thread in an application.

The Java ExecutorService interface is in the java.util.concurrent package. This interface represents an asynchronous execution mechanism to execute several tasks concurrently in the background.

Tasks performed by ExecutorService

The executor service framework helps in creating and managing threads in an application. The executor framework performs the following tasks.

1. Thread Creation: Executor service provides many methods for the creation of threads. This helps in running applications concurrently.

2. Thread Management: Executor service also helps in managing the thread life cycle. We need not worry if the thread is in the active, busy, or dead state before submitting the task for execution.

3. Task Submission And Execution: Executor framework also provides methods to submit tasks in the thread pool. It also provides the power to decide whether the thread will execute or not.

Task Delegation

The below diagram represents a thread delegating a task to a Java ExecutorService for asynchronous execution:

Task Delegation in Java

Creating an ExecutorService

ExecutorService is an interface in Java. The implementations of this interface can execute a Runnable or Callable class in an asynchronous way. We have to note that invoking the run() method of a Runnable interface in a synchronous way is calling a method.

We can create an instance of ExecutorService interface in the following ways:

1. Executors class

Executors class is a utility class that provides factory methods to create the implementations of the Executor service interface.

//Executes only one thread
ExecutorService es = Executors.newSingleThreadExecutor();

//Internal management of thread pool of 2 threads
ExecutorService es = Executors.newFixedThreadPool(2);

//Internally managed thread pool of 10 threads to run scheduled tasks
ExecutorService es = Executors.newScheduledThreadPool(10);

2. Constructors

The below statement creates a thread pool executor. We create it using the constructors with minimum thread count 10. The maximum thread count is 100. The keepalive time is five milliseconds. And, there is a blocking queue to watch for tasks in the future.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.LinkedBlockingQueue;
ExecutorService exService = new ThreadPoolExecutor(10, 100, 5L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue < Runnable > ());

Example of ExecutorService in Java

The ExecutorService in Java is a subinterface of the executor framework. It provides certain functions to manage the thread life cycle of an application. There is also a submit() method that can accept both runnable and callable objects.

In the below example, we will create an ExecutorService with a single thread and then submit the task to be executed inside the thread.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Example {
  public static void main(String[] args) {
    System.out.println("Inside: " + Thread.currentThread().getName());
    System.out.println("Creating ExecutorService");
    ExecutorService executorservice = Executors.newSingleThreadExecutor();
    System.out.println("Creating a Runnable");
    Runnable runnable = () - >{
      System.out.println("Inside: " + Thread.currentThread().getName());
    };
    System.out.println("Submitting the task specified by the runnable to the executorservice");
    executorservice.submit(runnable);
  }
}

Output:

Inside: main
Creating ExecutorService
Creating a Runnable
Submitting the task specified by the runnable to the executorservice
Inside: pool-1-thread-1

Note: When you run the above program, the program will never exit. You will need to shut it down explicitly since the executor service keeps listening for new tasks.

Java ExecutorService Implementations

ExecutorService is very similar to the thread pool. The implementation of the ExecutorService in the java.util.concurrent package is a thread pool implementation. There are following implementations of ExecutorService in the java.util.concurrent package:

1. ThreadPoolExecutor

The ThreadPoolExecutor executes the specified tasks using one of its internally pooled threads.

Thread Pool Executor in Java

Creating a threadPoolExecutor

int corethreadPoolSize = 10;
int maxPoolSize = 15;
long keepAliveTime = 6000;
ExecutorService es = new threadPoolExecutor(corethreadPoolSize, maxPoolSize, keepAliveTime, TimeUnit.MILLISECONDS, new LinkedBlockingQueue < Runnable > ());

2. ScheduledThreadPoolExecutor

The ScheduledThreadPoolExecutor is an ExecutorService that can schedule tasks to run after a delay or to execute repeatedly with a fixed interval of time in between each execution.

Creating a ScheduledthreadPoolExecutor

ScheduledExecutorService scheduledexecutorservice = Executors.newScheduledThreadPool(5);
ScheduledFuture scheduledfuture = scheduledExecutorService.schedule(new Callable() {
  public Object call() throws Exception {
    System.out.println("executed");
    return "called";
  }
},
5, TimeUnit.SECONDS);

ExecutorService Usage in Java

The following are the different ways to delegate tasks for execution to an ExecutorService:

  • execute(Runnable)
  • submit(Runnable)
  • submit(Callable)
  • invokeAny(…)
  • invokeAll(…)

1. Execute Runnable in java

The ExecutorService execute(Runnable) method of Java takes an object of Runnable and executes it asynchronously.

Below is an example of executing a Runnable with an ExecutorService:

ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(new Runnable() {
  public void run() {
    System.out.println("asynchronous task");
  }
});
executorService.shutdown();

2. Submit Runnable in java

The submit(Runnable) method takes a Runnable implementation and returns a Future object. We can use this Future object to check if the Runnable has finished executing.

Here is a Java ExecutorService submit() example:

Future future = executorService.submit(new Runnable() {
  public void run() {
    System.out.println(" asynchronous task ");
}
});
future.get();

3. Submit Callable in Java

The Java submit(Callable) method is similar to the submit(Runnable) method except it takes a Callable object instead of a Runnable. We can obtain the Callable’s result using the Java Future object returned by the submit(Callable) method.

Here is an ExecutorService Callable example:

Future future = executorService.submit(new Callable() {
  public Object call() throws Exception {
    System.out.println("Asynchronous callable");
    return "Callable Result";
  }
});
System.out.println("future.get() = "
future.get());

Output:

Asynchroous callable
future.get = Callable Result

4. invokeAny() in java

The invokeAny() method takes a collection or subinterfaces of Callable objects. This method returns the result of one of the Callable objects. There is no guarantee about which of the Callable results we will get.

For example:

public class ExecutorServiceExample {
  public static void main(String[] args) throws ExecutionException,
  InterruptedException {
    ExecutorService es = Executors.newSingleThreadExecutor();
    Set < Callable < String >> callable = new HashSet < Callable < String >> ();
    callable.add(new Callable < String > () {
      public String call() throws Exception {
        return "Task 1";
      }
    });
    callable.add(new Callable < String > () {
      public String call() throws Exception {
        return "Task 2";
      }
    });
    callable.add(new Callable < String > () {
      public String call() throws Exception {
        return "Task 3";
      }
    });
    String result = es.invokeAny(callable);
    System.out.println("result = " + result);
    executorService.shutdown();
  }
}

Output:

result = Task 1

5. invokeAll() in Java

The invokeAll() method invokes all of the objects of Callable that we pass to it in the collection as parameters. This method returns a list of Future objects through which we can obtain the results of the executions of each Callable.

For example:

public class ExecutorServiceExample {
  public static void main(String[] args) throws InterruptedException,
  ExecutionException {
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    Set < Callable < String >> callable = new HashSet < Callable < String >> ();
    callable.add(new Callable < String > () {
      public String call() throws Exception {
        return "Task 1";
      }
    });
    callable.add(new Callable < String > () {
      public String call() throws Exception {
        return "Task 2";
      }
    });
    callable.add(new Callable < String > () {
      public String call() throws Exception {
        return "Task 3";
      }
    });
    java.util.List < Future < String >> futures = executorService.invokeAll(callable);

    for (Future < String > future: futures) {
      System.out.println("future.get = " + future.get());
    }
    executorService.shutdown();

  }
}

Output:

future.get = Task 1
future.get = Task 3
future.get = Task 2

ExecutorService Shutdown in Java

When we compete using the Java ExecutorService we should shut it down, so the threads do not keep running. There are some situations when start an application via a main() method and the main thread exits our application.

In such cases, the application will keep running if there is an active ExecutorService in the application. These active threads present inside the ExecutorService prevents the JVM from shutting down.

Let’s discuss the methods to shut down an Executor service:

1. shutdown() in Java

We call the shutdown() method to terminate the threads inside the ExecutorService. This does not shut down the ExecutorService immediately, but it will no longer accept new tasks.

Once all the threads finish their current tasks, the ExecutorService shuts down. Before we call the shutdown() all tasks submitted to the ExecutorService are executed.

Below is an example of performing a Java ExecutorService shutdown:

executorService.shutdown();

2. shutdownNow() in Java

If we need to shut down the ExecutorService immediately, we can call the shutdownNow() method. This method will attempt to stop all executing tasks right away, and skip all the submitted but non-processed tasks.

But, there will be no guarantee about the executing tasks. They may either stop or may execute until the end.

For example:

executorService.shutdownNow();

3. awaitTermination() in Java

The ExecutorService awaitTermination() method blocks the thread calling it until either the ExecutorService has shutdown completely, or until a given time out occurs. The awaitTermination() method is typically called after calling shutdown() or shutdownNow().

Below is an example of calling ExecutorService awaitTermination() method:

executorService.awaitTermination();

Runnable vs. Callable Interface in Java

The Runnable interface is almost similar to the Callable interface. Both Runnable and Callable interfaces represent a task that a thread or an ExecutorService can execute concurrently. There is a single method in both interfaces.

There is one small difference between the Runnable and Callable interface. The difference between both the interfaces is clearly visible when we see the interface declarations.

Here is declaration of the Runnable interface:

public interface Runnable {
  public void run();
}

Here is declaration of the Callable interface:

public interface Callable {
  public Object call() throws Exception;
}

The main difference between the run() method of Runnable and the call() method of Callable is that call() can throw an exception, whereas run() cannot throw an exception, except the unchecked exceptions – subclasses of RuntimeException.

Another difference between call() and run() is that the call() method can return an Object from the method call.

Cancelling Task in Java

We can also cancel a Runnable or Callable task submitted to the ExecutorService of Java. We can cancel the task by calling the cancel() method on the Future. It is possible to cancel the task only if the task has not yet started executing.

For example:

Future.cancel();

Conclusion

Finally we saw ExecutorService helps in minimizing the complex code. It also helps in managing the resources by internally utilizing a thread pool. Programmers should be careful to avoid some common mistakes.

For example, always shutting down the executor service after the completion of tasks and services that are no longer needed. Otherwise, JVM will never terminate, normally. In this tutorial, we covered each and every concept of Executor service in Java.

TechVidvan Team

The TechVidvan Team delivers practical, beginner-friendly tutorials on programming, Java, Python, C++, DSA, AI, ML, data Science, Android, Flutter, MERN, Web Development, and technology. Our experts are here to help you upskill and excel in today’s tech industry.