Working solely with Thread instances can be challenging. Threads must be correctly started, stopped and disposed of. We often need to synchronize threads, schedule tasks, create thread pools and devise a means of communication between threads. All this requires complex and error-prone coding.
Luckily, Java introduced the Concurrency API, which allows us to use threads without directly using the Thread class. This API, among other classes, contains the ExecutorService interface, which creates and manages threads for us. Moreover, it automatically provides features such as thread pooling and scheduling, simplifying the concurrent execution of tasks.
In this tutorial, we’ll learn about the ExecutorService class. First, we’ll explore ways to instantiate it. Then we’ll see how to submit tasks for execution. Finally, we’ll examine how to shut down the ExecutorService once we no longer need it.
Creating ExecutorService
Based on the number of threads, we distinguish single-threaded and pool-threaded executors. Let’s discuss how to create each.
Single-Threaded Executor
As the name suggests, a single-threaded executor represents an executor that uses a single thread. We should use the ExecutorService every time we execute the task in a separate thread, even if we need a single thread.
One way to instantiate such an executor is by using factory methods provided in the Executor class:
- The newSingleThreadExecutor() method represents the most straightforward way to instantiate the ExecutorService. It creates an executor that uses a single thread operating on a queue.
- The newSingleThreadScheduledExecutor() method creates a new instance of ScheduledExecutorService that can execute tasks periodically or after a given delay.
When working with a single-threaded executor, the tasks will be executed in the order in which we added them to the executor service.
It’s also important to note that the main() method executes in a different thread that has nothing to do with the ExecutorService. In other words, it can perform tasks while other threads (managed by the ExecutorService) are running.
Let’s create an example of a single-threaded executor:
ExecutorService service = Executors.newSingleThreadExecutor();
If we have multiple tasks we want to execute, other tasks will be postponed until the currently running task is completed.
Pool-Threaded Executor
As previously mentioned, if a single-threaded executor needs to execute multiple tasks, it’ll wait for a thread to become available before running the next task. On the contrary, we can perform various tasks concurrently using a pool-threaded executor.
To create an instance of the pool-threaded executor, we can use several factory methods provided in the Executor class:
- The newCachedThreadPool() method allocates a new thread whenever required. This method can be used to execute many short-lived asynchronous tasks.
- The newFixedThreadPool() method allocates a given number of threads upon creation. If the number of tasks doesn’t exceed the allocated threads, it’ll execute all tasks concurrently.
- The newScheduledThreadPool() method behaves the same as the newFixedThreadPool() method, except it returns an executor we can use to schedule tasks.
Let’s create a pool-threaded executor:
ExecutorService service = Executors.newFixedThreadPool(5);
Submitting Tasks
Now let’s explore the methods to submit tasks for execution.
The simplest way to submit a task is by using the execute() method:
public class PoolThreadedExecutor {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(5);
service.execute(() -> System.out.println("Task 1"));
service.execute(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("Task 2: " + i);
}
});
service.execute(() -> System.out.println("Task 3"));
}
}
Here, we created a pool of five threads. Furthermore, we defined three tasks that will be executed concurrently since the number of tasks is less than the number of available threads. This method works on the fire-and-forget principle, which doesn’t return a value.
That’s to say, once submitted, the results aren’t available to the calling thread.
However, if we need an object to determine whether the task is complete, we can use the submit() method instead:
public static void main(String[] args) {
try {
Callable<String> task = () -> "Avocado";
ExecutorService service = Executors.newFixedThreadPool(5);
Future<String> result = service.submit(task);
while (true) {
if (result.isDone()) {
System.out.println(result.get());
break;
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
The submit() method accepts either Runnable or Callable and returns the Future. Then, we can use the result to determine whether the task is complete.
Let’s explore the other methods we can use when submitting tasks:
- The invokeAll() method – Executes the given tasks, synchronously returning the result of all tasks in the same order they were in the original collection.
- The invokeAny() method executes given tasks, synchronously returning the result of one of the finished tasks and canceling any unfinished tasks.
- The schedule() method – Runs task after a given delay.
- The scheduleAtFixedRate() method – Runs the task for every given period.
- The scheduleAtFixedDelay() method – Creates and executes tasks after a given initial delay.
Shutting Down a Thread Executor
Lastly, let’s show the importance of shutting down the executor service. Resources such as executors should be closed to prevent memory leaks.
Let’s consider the following example:
ExecutorService service = Executors.newFixedThreadPool(500);
for (int i = 0; i < 500; i++) {
service.execute(() -> System.out.println("Doing some work"));
}
// Simulating application running
Thread.sleep(1000000);
Here, we created 500 threads to execute some tasks. Then, we called the Thread.sleep() method to simulate the application running.
Threads we created may remain alive, preventing GC, which can lead to memory leaks.
Let’s take a thread dump and upload it to the fastThread tool,which will give us an in-depth analysis of the dump as well as making useful recommendations . The report shows the warning that we’re not using 90% of our threads:
Fig: Thread Pools showing 90% of unused threads
The entire report is available here.
We can fix the problem by calling the shutdown() method:
ExecutorService service = null;
try {
service = Executors.newFixedThreadPool(500);
for (int i = 0; i < 500; i++) {
service.execute(() -> System.out.println("Doing some work"));
}
} finally {
if (service != null) {
service.shutdown();
}
}
// Simulating application running
Thread.sleep(1000000);
The shutdown() method first rejects any new task submitted for execution while continuing to execute the ongoing task.
In addition, uploading the thread dump of modified code to the fastThread tool no longer shows a potential problem. We can check the entire report here.
Alternatively, we can use the shutdownNow() method to try to stop all running tasks and those that have yet to be started.
Conclusion
This article taught us how to work with single-threaded and pool-threaded executor services.
To summarize, we saw how to instantiate the ExecutorService, run tasks, and properly close it once we no longer need it. Lastly, we learned how to diagnose potential memory leaks using the fastThread tool.

Share your Thoughts!