Guide to the Java ExecutorService

In this article, we will look at the Java ExecutorService. The ExecutorService framework in Java provides a powerful and flexible option for asynchronous task execution.

 

Introduction

Multi-threading is an important part of Java. Using multi-threading or by running multiple threads parallel, we can reduce the extra load of an application. Multi-threading is difficult to implement. JDK provides one framework called Executors to overcome this problem. Thread creation is an expensive task and Executors framework helps us to reuse the already created threads. Let’s look at the different ExecutorService in Java.

 

1. Java ExecutorService Types

Java ExecutorService framework centred around Executor interface with its sub-interfaces ExecutorService, ScheduledExecutorService and ThreadPoolExecutor. ExecutorService can execute a thread or runnable and callable objects asynchronously. ScheduledExecutorService is an ExecutorService sub interface that can schedule a task. It can schedule tasks to run after a specific time or it can execute the tasks repeatedly with an interval. Following are the classes that implements ExecutorService:

  1. ThreadPoolExecutor.
  2. ScheduledThreadPoolExecutor.
  3. ForkJoinPool.
  4. AbstractExecutorService

 

2. Runnable, Callable Interface

We can submit one Callable or Runnable interface task to an Executor. The Runnable interface defines one ‘run()’ method. This method doesn’t take any parameters, it doesn’t return any values. Also, it can’t throw a checked exception. They introduced Runnable in Java 1. they introduced Callable in Java 1.5. It defines a call() method which can throw a checked exception. Callable objects are used if you want the results after completion. If you pass one Callable object to an Executor, it returns one Future object. This object has different methods to check the status.

The drawback with Callable is that it doesn’t allow to register for a callback. It is an asynchronous task. The only way to know the status is by querying the Future object for the status. Java 8 introduced one new interface called CompletableFuture. This class implements the Future interface, it provides a way to listen for the completion.

 

3. Future Interface

Use future objects with asynchronous tasks. We return the result of an asynchronous task as a Future object. We can check the status and access the result using its built-in methods. isDone() method is used to do a quick look at the status, get() method is used to get the result and cancel() method is used to stop the task. So, future objects are used by the main thread to keep track of the progress of a task on a separate thread.

 

4. ThreadPoolExecutor

ThreadPoolExecutor implements ExecutorService that can handle the processing of each submitted task on several thread pools.It also has one ScheduledThreadPoolExecutor that can execute after a delay or repeatedly. We can set the core pool size along with the maximum pool size. Based on these values, it can automatically adjust the pool size. When a new task is submitted, if the running threads are fewer than core pool size, a new thread is created for the new task always even if a worker thread is idle. If the running threads are more than core pool size and less than maximum pool size, they create only one new thread if the task queue is full.

One more useful configuration of ThreadPoolExecutor is the keep-alive time. If the total threads are more than core pool size, the excess threads will be terminated if they are in an idle mode for more than keep-alive time. Following are the property variables to define these terms:

  1. corePoolSize.
  2. maximumPoolSize.
  3. keepAliveTime

Following are the getter, setter for each: 

  1. public int getCorePoolSize()setCorePoolSize(int)
  2. public int getMaximumPoolSize() , setMaximumPoolSize(int)
  3. public long getKeepAliveTime(TimeUnit) and setKeepAliveTime(TimeUnit)

 

4.1 Example of ExecutorService

ThreadPoolExecutor class implements the ExecutorService interface. Executors class provides several methods to create one single or thread pool. Following are the most commonly used methods:

  1. newFixedThreadPool(int n): It creates one thread pool that reuses fixed ’n’ number of threads.
  2. newScheduledThreadPool(int corePoolSize): Creates one scheduled thread pool or a thread pool that can execute with a delay or periodically.
  3. newSingleThreadExecutor(): Create one executor with one worker thread.
  4. newSingleThreadScheduledExecutor(): Create scheduled executor with one worker thread.
  5. newCachedThreadPool(): Creates one thread pool that reuses previously constructed threads if available and creates new threads if not available.

We can create a task that implements Runnable or Callable and pass it to an ExecutorService. Note that the ThreadPoolExecutor class also has different constructors, but the above utility methods of Java Executors class used in most cases. Let’s look at the below example:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class ExampleTask implements Runnable {

    private int id;
    ExampleTask(int id) {
        this.id = id;
    }

    @Override
    public void run() {

        try {

            System.out.println("Started : " + id);
            Thread.sleep(5000);
            System.out.println("Completed : " + id);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

    }

}

public class ExecutorServiceExample {

    public static void main(String[] args) {

        ExecutorService service = Executors.newFixedThreadPool(2);

        for (int i = 0; i < 10; i++) {

            service.execute(new ExampleTask(i));

        }

        service.shutdown();
    }

}

When we run this code, we have the following output:

Started : 0 
Started : 1 
Completed : 0 
Completed : 1 
Started : 2 
Started : 3 
Completed : 3 
Completed : 2 
Started : 4 
Started : 5 
Completed : 4 
Started : 6 
Completed : 5 
Started : 7 
Completed : 6 
Completed : 7 
Started : 8 
Started : 9 
Completed : 8 
Completed : 9

Let’s cover some important points before moving to the next section:

  • We are creating one fixed thread pool with 2 threads.
  • It creates 10 tasks, submits all to the executor queue using a for-loop. During the execution time, you notice that we start only two threads max. Each runnable sleeps for 5 seconds. So, the next two threads executed only after it completes the previous two.
  • shutdown() method is called immediately after the for loop runs. It stops the ExeuctorService once it executes all tasks. If you don’t stop one executor class, it will not end even after the program ends. It will wait continuously for any other tasks to consume.

 

4.2 ExecutorService with Multiple Threads

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class TaskOne implements Runnable {

    @Override
    public void run() {

        System.out.println("Running task 1");
        try {

            Thread.sleep(5000);

        } catch (InterruptedException e) {

            e.printStackTrace();
        }
    }
}

class TaskTwo implements Runnable {

    @Override
    public void run() {

        System.out.println("Running task 2");
        try {

            Thread.sleep(5000);

        } catch (InterruptedException e) {

            e.printStackTrace();
        }
    }
}

class TaskThree implements Runnable {

    @Override
    public void run() {

        System.out.println("Running task 3");
        try {

            Thread.sleep(5000);

        } catch (InterruptedException e) {

            e.printStackTrace();
        }
    }
}

public class ExecutorServiceMultiThread {

    public static void main(String[] args) {

        ExecutorService service = Executors.newFixedThreadPool(3);
        TaskOne firstTask = new TaskOne();
        TaskTwo secondTask = new TaskTwo();
        TaskThree thirdTask = new TaskThree();

        service.execute(firstTask);
        service.execute(secondTask);
        service.execute(thirdTask);

        service.shutdown();
    }
}

We have three different Runnable tasks that we are pushing to an ExecutorService. All tasks will execute parallel as we set the thread pool size is 3.

4.3. ExecutorService with Callable

If you pass one Callable task to a thread pool, it returns one Future object. We have several methods to execute callable tasks:

  1. Future submit(task): Submit one Callable task that returns one pending Future object.
  2. List invokeAll(tasks): Submit a list of Callable tasks. It returns a list of Future objects for each task. We can get the status of each task using these objects. We can pass one long timeout as the second parameter and one TimeUnit as the third parameter.
  3. T invokeAny(tasks): It returns the result of one task that was completed successfully. Similar to invokeAll, we can pass one timeout as the second parameter and one TimeUnit as the third parameter.
import java.util.concurrent.*;

class CallableTask implements Callable < Integer > {

    private int id;

    CallableTask(int id) {
        this.id = id;
    }

    @Override
    public Integer call() {

        try {

            TimeUnit.MILLISECONDS.sleep(5000);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return id;
    }
}

public class ExecutorServiceCallableExample {

    static void printFutureData(Future future) {

        System.out.println("isDone : " + future.isDone());

        try {
            System.out.println("get : " + future.get());

        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        ExecutorService service = Executors.newFixedThreadPool(2);
        Future future = service.submit(new CallableTask(1));
        service.shutdown();

        while (!future.isDone()) {

            try {
                System.out.println("Waiting for the Future to complete ...");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        printFutureData(future);
    }
}

Let’s discuss some important points for the above example:

  1. CallableTask is a callable task. It is implementing the Callable interface, overriding the call() method.
  2. In the constructor, we are passing one integer id and the call() method returns that id.
  3. printFutureData() is an utility method that takes one Future object, prints the return of isDone() and get() methods. isDone() returns one boolean value stating if the task is complete or not. get() returns the computed result i.e. the id in our case.
  4. The main method submits one Callable object to the ExecutorService and with one while loop, it waits for the Future object to complete. Note that we are calling the shutDown() method immediately after submitting the task. Without it, the program will not stop.

When we run the above code, you can see similar output:

Waiting for the Future to complete ... 
Waiting for the Future to complete ... 
Waiting for the Future to complete ... 
Waiting for the Future to complete ... 
Waiting for the Future to complete ... 
isDone : true 
get : 1 

As you can see here, the while loop waits for the Future object to complete before it prints the content of the Future object.

4.4. ScheduledExecutorService Example

ScheduledExecutorService is useful if you want to execute a task repeatedly for a definite or indefinite time. The below methods are used to execute a task:

  1. ScheduledFuture schedule(Callable callable,long delay,TimeUnit timeunit) : It creates and executes a ScheduleFuture after a delay.
  2. ScheduledFuture schedule(Runnable runnable,long delay,TimeUnit timeunit) : Creates, executes one task after a delay.
  3. ScheduledFuture scheduleAtFixedRate(Runnable runnable,long initialdelay,long period,TimeUnit timeunit): Enables a task after an initial delay. It continue running it on period interval.
  4. ScheduledFuture scheduleWithFixedDelay(Runnable runnable, long initialdelay, long delay, TimeUnit timeunit): The periodic action enables after the initial delay and after the delay period.
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

class RunnableTask implements Runnable {

    long startTime;

    RunnableTask() {
        startTime = System.currentTimeMillis();
    }

    @Override
    public void run() {
        System.out.println("Seconds : " + ((System.currentTimeMillis() - startTime) / 1000));
    }
}

public class ScheduledThreadPoolExecutorExample {

    public static void main(String[] args) {

        ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
        executor.scheduleWithFixedDelay(new RunnableTask(), 3, 2, TimeUnit.SECONDS);

    }
}
  1. We have created one Runnable class RunnableTask.
  2. scheduleWithFixedDelay method is used to execute one RunnableTask object with initial delay 3 seconds and delay 2 seconds.

Output

Seconds: 3 
Seconds: 5 
Seconds: 7 
Seconds: 9 
Seconds: 11 
… 

 

5. How to stop an ExecutorService

If we don’t stop an ExecutorService, it will keep waiting for more tasks. ExecutorService provides a few different methods to shut down an executor :

  1. shutdown(): Shut down the ExecutorService once the previously submitted tasks are complete without accepting any new tasks.
  2. shutdownNow(): It attempts to stop all running tasks and returns one list of all pending tasks. It shuts down the executor immediately.
  3. awaitTermination(long timeout, TimeUnit unit): Blocks the calling thread until the tasks are complete or a timeout occurs.
  4. isTerminated(): If you call shutdown() or shutdownNow(), it returns true if the shutting down is complete.
  5. isShutdown(): Returns true if you have called the shutdown() method.

 

6. Difference between ExecutorService vs Fork/Join

Java 7 introduced the Fork/Join framework. This framework uses a different thread pool called ForkJoinPool that is used to run ForkJoinTasks. A ForkJoinPool is an ExecutorService in java.util.concurrent package. We can create thread pool for both Executor framework and Fork/Join framework.

The main difference between ForkJoinPool and ExecutorService is the way each thread pool works. For ExecutorService, the thread pool executes any task if any worker thread is available. But for ForkJoinPool, the thread pool uses a different method called work-stealing to handle tasks. All threads in the pool attempts to find and execute submitted tasks. Another difference is that ForkJoinPool dynamically adds, suspends, resumes internal worker threads to maintain enough active threads.

 

8. FAQ

Before we summarize our article, let’s revisit some core concepts of this article.

What is an ExecutorService?

ExecutorService framework allows us to pass a task for the asynchronous execution. Multi-threading is difficult to implement and JDK provides one framework called Executors to overcome this problem

Is ExecutorService asynchronous?

Executors framework centred around Executor interface with its sub-interfaces ExecutorService, ScheduledExecutorService and ThreadPoolExecutor. ExecutorService can execute a thread or runnable and callable objects asynchronously.

 

Summary

In this article, we discussed the Java ExecutorService. ExecutorService makes it easier to implement multi-threaded applications. But few things we always need to keep in the mind like shutting down the service if not in use, handling the exceptions properly, creating the thread pool with minimum required thread, etc. Also, timeout should be used to avoid an indefinite wait.The source code for this article is available on GitHub.

Manish Sharma

Manish's primary interests are Java, Spring Boot and Spring. His focus is more toward the automations and testing.Manish love travelling and when not working, he might be exploring some new destination.

Leave a Reply

avatar

This site uses Akismet to reduce spam. Learn how your comment data is processed.

  Subscribe  
Notify of