This article is part of my Java Concurrency tutorial series.

Welcome to the third part of my tutorial series on Java concurrency. In this tutorial we will learn how to manage threads in our application using executors and thread pools.

Executors

In the previous tutorial, we learned how to create and manage threads in Java. While it is easy to create one or two threads and run them, it becomes a problem when your application has 20 or 30 threads that you have to manage.

Also, it won’t be exaggerating to say that large multithreaded applications will have hundreds or thousands of threads running simultaneously. So, it makes sense to separate thread creation and management from the rest of the application.

Enter Executors, A framework for creating and managing threads.

Java Concurrency API defines the following three executor interfaces that covers everything that is needed for creating and managing threads -

  • Executor - A simple interface that contains a method called execute() to launch a task specified by a Runnable object.

  • ExecutorService - A sub-interface of Executor that adds functionality to manage the lifecycle of the tasks. It also provides a submit() method whose overloaded versions can accept a Runnable as well as a Callable object. Callable objects are similar to Runnable except that the task specified by a Callable object can also return a value. We’ll learn about Callable in more detail, in the next blog post.

  • ScheduledExecutorService - A sub-interface of ExecutorService. It adds functionality to schedule the execution of the tasks.

Apart from the above three interfaces, The api also provides an Executors class that contains factory methods for creating different kinds of executor services.

All right! let’s dive into an example now to understand things better -

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

public class ExecutorsExample {
    public static void main(String[] args) {
        System.out.println("Inside : " + Thread.currentThread().getName());

        System.out.println("Creating Executor Service...");
        ExecutorService executorService = Executors.newSingleThreadExecutor();

        System.out.println("Creating a Runnable...");
        Runnable runnable = () -> {
            System.out.println("Inside : " + Thread.currentThread().getName());
        };

        System.out.println("Submit the task specified by the runnable to the executor service.");
        executorService.submit(runnable);
    }
}
# Output
Inside : main
Creating Executor Service...
Creating a Runnable...
Submit the task specified by the runnable to the executor service.
Shutting down the executor
Inside : pool-1-thread-1

The above example shows how to create an executor service and execute a task inside the executor.

At this point, you might be wondering that what is the advantage of using an executor service here. You could have simply done the following to execute the task -

new Thread(runnable).start()

Yeah, that’s true. The advantage of executor service is not apparent when you have a single task that you want to execute. But, If your application has hundreds of tasks, then each time you have to execute a task, you need to create a new thread. You cannot reuse an existing thread, because, once a thread is done executing a task, it dies.

But, with executorService, you can submit as many tasks as you want without worrying about whether the thread used by the executor is active or dead.

Executor service consists of a pool of threads. When a new task is submitted, it picks one of the available threads from the pool and executes the task on that thread instead of creating a new thread every time.

Following example shows how you can execute multiple tasks inside an executor service -

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

public class ExecutorsExample {
    public static void main(String[] args) {
        System.out.println("Inside : " + Thread.currentThread().getName());

        System.out.println("Creating Executor Service...");
        ExecutorService executorService = Executors.newSingleThreadExecutor();

        System.out.println("Creating a Runnable...");
        Runnable runnable = () -> {
            System.out.println("Executing first task inside : " + Thread.currentThread().getName());
        };

        System.out.println("Submit the task specified by the runnable to the executor service.");
        executorService.submit(runnable);

        System.out.println("Creating another Runnable...");
        Runnable runnable2 = () -> {
            System.out.println("Executing second task inside : " + Thread.currentThread().getName());
        };

        System.out.println("Submitting second runnable.");
        executorService.submit(runnable2);
    }
}
# Output
Inside : main
Creating Executor Service...
Creating a Runnable...
Submit the task specified by the runnable to the executor service.
Creating another Runnable...
Executing first task inside : pool-1-thread-1
Submitting second runnable.
Executing second task inside : pool-1-thread-1

Notice the output of the above program. See how both Runnables are executed inside the same thread pool-1-thread-1.

If you run the above program, you will notice that the program never exits, because, the executor service keeps listening for new tasks until we shut it down explicitly.

ExecutorService provides two methods for shutting down an executor -

  • shutdown() - when shutdown() method is called on an executor service, it stops accepting new tasks, waits for previously submitted tasks to execute, and then terminates the executor.

  • shutdownNow() - this method interrupts all the running tasks and shut down the executor immediately.

Let’s add shutdown code at the end of our program so that it exits gracefully -

System.out.println("Shutting down the executor");
executorService.shutdown();

Thread Pool

Most of the executor implementations use thread pools to execute tasks. A thread pool is nothing but a bunch of worker threads that exist separately from the Runnable or Callable tasks and is managed by the executor.

Creating a thread is an expensive operation and it should be minimized. Having worker threads minimizes the overhead due to thread creation, because executor service has to create the thread pool only once and then it can reuse the threads for executing any task.

In the Executor example above, the method Executors.newSingleThreadExecutor(), creates an executor with a thread pool of size one.

One common type of thread pool that is frequently used in multithreaded applications is a fixed thread pool. You can create a fixed thread pool by using the newFixedThreadPool() method of Executors class.

ExecutorService executorService = Executors.newFixedThreadPool(10);

The above executor service contains a fixed thread pool of size 10. It makes sure that the pool always has the specified number of threads running. If any thread dies due to some reason, it is replaced by a new thread immediately.

Now, You might ask that what happens if all the threads in the pool are busy and a new task is submitted to the executor service?

Well! Tasks are submitted to the thread pool via an internal queue called the Blocking Queue. If there are more tasks than the number of active threads, they are inserted into the blocking queue for waiting until any thread becomes available. If the blocking queue is full than new tasks are rejected.

Java Executor Service and Thread Pool Example

Scheduled Executors

ScheduledExecutorService is used to execute a task either periodically or after a specified delay.

In the following example, We schedule a task to be executed after a delay of 5 seconds -

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledExecutorsExample {
    public static void main(String[] args) {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        Runnable task = () -> {
          System.out.println("Executing Task At " + System.nanoTime());
        };

        System.out.println("Submitting task at " + System.nanoTime() + " to be executed after 5 seconds.");
        scheduledExecutorService.schedule(task, 5, TimeUnit.SECONDS);
        
        scheduledExecutorService.shutdown();
    }
}
# Output
Submitting task at 2909896838099 to be executed after 5 seconds.
Executing Task At 2914898174612

scheduledExecutorService.schedule() function takes a Runnable, a delay value, and the unit of the delay. The above program executes the task after 5 seconds from the time of submission.

Now let’s see an example where we execute the task periodically -

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledExecutorsPeriodicExample {
    public static void main(String[] args) {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);

        Runnable task = () -> {
          System.out.println("Executing Task At " + System.nanoTime());
        };
        
        System.out.println("scheduling task to be executed every 2 seconds with an initial delay of 0 seconds");
        scheduledExecutorService.scheduleAtFixedRate(task, 0,2, TimeUnit.SECONDS);
    }
}
# Output
scheduling task to be executed every 2 seconds with an initial delay of 0 seconds
Executing Task At 2996678636683
Executing Task At 2998680789041
Executing Task At 3000679706326
Executing Task At 3002679224212
.....

scheduledExecutorService.scheduleAtFixedRate() method takes a Runnable, an initial delay, the period of execution, and the time unit. It starts the execution of the given task after the specified delay and then executes it periodically on an interval specified by the period value.

Note that if the task encounters an exception, subsequent executions of the task are suppressed. Otherwise, the task will only terminate if you either shutdown the executor or kill the program.

Conclusion

In this blog post we learned the basics of executors and thread pool. However, we have not yet covered all the features that executor service offers, because for covering those features, we first need to understand two more topics - Callable and Future. We’ll cover these topics in the next blog post.

All the code samples used in this tutorial can be found in my github repository. Please ask any doubts or clarifications in the comment section below.