A Guide to the Java ExecutorService

May 09, 2022
xblog JavaExecutor



Introduction to Java ExecutorService

Java programming language has been a great option for developing multi-threaded applications. This is because Java offers numerous features for multi-threaded development.

For instance, as it often gets very complicated to execute a large number of threads simultaneously in an application, Java offers the Executor Framework for threads management.

The Executor Framework comes with Java ExecutorService. It is a sub-interface that offers a very simplified and easy approach to multi-threading in asynchronous mode.

In this article, we will be exploring the Java ExecutorService. We will be looking into its different methods of instantiation, how to manually assign tasks to thread and will look into various factory methods available with Java ExecutorService.

new java job roles

Java Executor Framework

It is fairly easier to create and execute hand full of threads simultaneously but it gets complicated when the number of threads will increase to a significant number.

Large multi-threaded applications often deal with hundreds of threads running simultaneously. To manage these threads and the tasks associated with them, it makes sense to separate the thread creation and management process.

The executor framework separately handles the following tasks for managing a multi-threaded application.

  • Thread Creation – There is a variety of factory methods available for the creation of the threads.
  • Thread Management – The thread life cycle is also managed by the executor framework separately. It keeps track of every thread in the thread pool whether it is active, busy or dead before submitting it for execution.
  • Task Submission and Execution – Some factory methods for submitting a task in the thread pool are also available.

Java ExecutorService

Multi-threading is not very simple to implement. It is also a very expensive task in terms of resources especially when it comes to threads creation.

To cater to that, Java ExecutorService allows you to reuse the already created threads. This sub-interface offers various functionalities for managing the thread life cycle of a multi-threaded application.

It provides a pool of threads along with an API for assigning tasks to each thread. If there are a greater number of tasks than available threads in the pool, it also provides the option to queue up all the waiting tasks until a thread is available.

Java ExecutorService Implementations

As Java ExecutorService is an interface, it needs to be implemented to make any use of it.

The Java ExecutorService has the following two implementations in the java.util.concurrent package:

1. ThreadPoolExecutor

The ThreadPoolExecutor implementation executes a given tasks using threads from the internal pool.

 

See this code example below Creating a threadPoolExecutor:

1. int corePoolSize = 10;
2. int maxPoolSize = 20;
3. long keepAliveTime = 4000;
4. ExecutorService threadPoolExecutor = new threadPoolExecutor( corePoolSize, 
   maxPoolSize, keepAliveTime, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())

2. ScheduledThreadPoolExecutor

The java.util.concurrent.ScheduledThreadPoolExecutor is another Java ExecutorService implementation that can be used to schedule tasks to run after a certain delay or to repeatedly execute the tasks after a fixed interval of time.

 

Here is an example of using ScheduledThreadPoolExecutor implementation to create 10 threads where each is called after every 10 seconds:

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

Creating a Java ExecutorService

There are multiple ways for creating a Java ExecutiveService but it cannot be selected before the implementation as the approach for creating a Java ExecutorService varies with different implementations.

Also Read: A Guide to Java Sockets

A general and also the simplest method is using the Executors factory class to create ExecutorService instances that can be used with the factory methods to create a thread pool.

 

Following are a few examples of creating a Java ExecutorService:

ExecutorService es1 = Executors.newSingleThreadExecutor();

ExecutorService es2 = Executors.newFixedThreadPool(10);

ExecutorService es3 = Executors.newScheduledThreadPool(10);

Factory methods in Java ExecutorService

Following are few of numerous factory methods used to work with tasks and threads:

  • Submit(Runnable)
  • Execute(Runnable)
  • Submit(Callable)
  • Invokeany( )
  • Invokeall( )
  • Cancel()

1. Execute (Runnable)

The Java ExecutorService execute(Runnable) method takes a java.lang.Runnable object, and executes it asynchronously.

 

See this example below, executing a Runnable with a Java ExecutorService:

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

It is to be noted that you cannot obtain the result of the executed Runnable. It will require the use of Callable.

2. Submit (Runnable)

The Java ExecutorService submit(Runnable) method also takes a java.lang.Runnable object, but it returns a Future object. This Future object can be then used to check the status of the Runnable execution.

 

See this example:

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

3. Submit (Callable)

The Java ExecutorService submit(Callable) method is very similar to the submit(Runnable) method but it takes a Java Callable instead of a Runnable.

We will be discussing the difference between a Callable and a Runnable later in this article.

 

The Callable result can be obtained via 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 "result is Callable";
     }
});

System.out.println("future.get() = " + future.get());

 

The above code example will output this:

Asynchronous Callable

future.get() = result is Callable

4. InvokeAny()

The invokeAny() method takes a collection of Callable objects and returns the result of one of the Callable objects selected randomly.

As any one of the Callable finishes when the task is completed, a result is returned from invokeAny()and the rest of the Callable instances are cancelled right away.

 

See this code example:

ExecutorService executorService = Executors.newSingleThreadExecutor();
Set<Callable<String>> callables = new HashSet<Callable<String>>();
callables.add(new Callable<String>() {
      public String call() throws Exception {
          return "task 01";
      }
});
callables.add(new Callable<String>() {
      public String call() throws Exception {
           return "task 02";
      }
});

String result = executorService.invokeAny(callables);
System.out.println("result = " + result);
executorService.shutdown();

5. invokeAll()

The invokeAll() method invokes all of the Callable objects passed in the collection, passed as parameters. It returns a list of Future objects that can be used to get the results of the executions of every Callable.

It is to be noted that a task may be unsuccessful but there is no way possible to identify it.

 

See this example:

ExecutorService executorService = Executors.newSingleThreadExecutor();
Set<Callable<String>> callables = new HashSet<Callable<String>>();
callables.add(new Callable<String>() {
     public String call() throws Exception {
           return "task 01";
     }
});
callables.add(new Callable<String>() {
     public String call() throws Exception {
          return "task 02";
     }
});

List<Future<String>> futures = executorService.invokeAll(callables);

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

executorService.shutdown();

6. Cancel ()

As the name suggests, the cancel() method allows you to cancel a submitted task by calling the method on the Future returned right when the task is submitted. A task can only be cancelled if it has not started executing.

 

See this:

future.cancel();

Runnable vs Callable

They both are quite similar but the Runnable interface represents a task that can be executed by a thread or by an ExecutorService whereas Callable can only be executed by an ExecutorService.

Both interfaces only have a single method but the call() method in callable can return an Object from the method call but that is not possible by the run() method.

Another difference between call() and run() is that call() can throw an exception, whereas run() cannot, except for unchecked exceptions.

All these differences might be confusing but to be clear, if you need to submit a task to a Java ExecutorService and you want to get the result back from the task, then you need to make your task implement the Callable interface otherwise you can go with any of them.

ExecutorService Shutdown methods

You must shut down the Java ExecutorService after using it, so the threads do not keep running in the background.

Following are the three shutdown methods,

1. Shutdown( )

It will not shut down The ExecutorService immediately, but it will stop accepting new tasks, and once all threads have finished their tasks, the ExecutorService will shut down.

executorService.shutdown();

2. ShutdownNow()

It will stop all executing tasks and will skip all the submitted but non-processed tasks right away. Despite that, the executing tasks still can decide to execute until the end or to stop instantly.

executorService.shutdownNow();

3. AwaitTermination()

The awaitTermination() method is usually called after the shutdown method. It blocks the thread calling until either the ExecutorService has shut down completely, or for a specific time passed as a parameter.

executorService.shutdown();

executorService.awaitTermination(10_000L, TimeUnit.MILLISECONDS);

Conclusion

From creation to termination, we have covered every aspect of Java ExecutorService.

ExecutorService is an exceptional framework and the Java ExecutorService can certainly be a very useful addition to your Java toolkit if you are currently working with multi-threaded applications or wants to pursue its development in Java.

new Java jobs



author

jordan

Full Stack Java Developer | Writer | Recruiter, bridging the gap between exceptional talent and opportunities, for some of the biggest Fortune 500 companies.


Candidate signup

Create a free profile and find your next great opportunity.

JOIN NOW

Employer signup

Sign up and find a perfect match for your team.

HIRE NOW

How it works

Xperti vets skilled professionals with its unique talent-matching process.

LET’S EXPLORE

Join our community

Connect and engage with technology enthusiasts.

CONNECT WITH US