1. Overview

In this tutorial, we'll look at how we can shut down a thread pool using Java ExecutorService. We'll evaluate two different approaches: graceful termination via ExecutorService.shutdown and abrupt termination via ExecutorService.shutdownNow.

2. Graceful Termination using shutdown

We'll first use the shutdown method of ExecutorService to terminate a thread pool gracefully.

When we invoke shutDown, the thread pool stops accepting new tasks. Then, it waits for already submitted tasks to complete even if they didn't start yet.  

public void shutdown() {
    final ExecutorService threadPool = Executors.newFixedThreadPool(5);

    for (int i = 0; i < 100; i++) {
        threadPool.execute(() -> {
            if (threadPool.isShutdown()) {
                System.out.println("Pool is terminating.");
            }
            
            System.out.println("Doing work.");
        });
    }

    threadPool.shutdown();
}

Here, we're creating a thread pool with five threads. Then, we're executing some tasks that output a message if the pool is terminating. We're expecting to see some of these messages since shutdown allows threads to continue and complete their work.

A sample run prints:

Doing work.
Doing work.
Doing work.
...
Pool is terminating.
Doing work.
Pool is terminating.
Doing work.

As we can see, some of the tasks are still running after we requested a shutdown.

Note that our task doesn't contain any blocking I/O operation or doesn't have a loop. So it'll eventually complete and the thread pool will terminate. On the other hand, if a task contains a blocking operation or loop, it may not stop since shutdown doesn't attempt to cancel running tasks. As a result, the thread pool may not terminate.

As an example, we'll next execute a task containing a loop:

public void shutdownTheLooperRespectingInterrupt() throws InterruptedException {
    final ExecutorService threadPool = Executors.newFixedThreadPool(5);
    threadPool.execute(() -> {
        while (!Thread.currentThread().isInterrupted()) {
            doWork();
        }
    });

    threadPool.shutdown();
}

Here, we're creating a thread pool and submitting our looper task. When we invoke shutdown, the thread pool starts waiting for the task to complete. But this never happens, since the worker thread isn't interrupted. Thus the thread pool doesn't terminate.

3. Abrupt Termination using shutdownNow

Another option to terminate a thread pool is via the shutdownNow method. This results in an abrupt termination because, unlike the shutdown method, shutdownNow tries to cancel running tasks. It also removes the waiting tasks from the queue and returns to the caller - possibly to store the state and retry later:

public void shutdownNow() throws InterruptedException {
    final ExecutorService threadPool = Executors.newFixedThreadPool(5);

    for (int i = 0; i < 100; i++) {
        threadPool.execute(() -> {
            if (threadPool.isShutdown()) {
                System.out.println("Pool is terminating.");
            }

            System.out.println("Doing work.");
        });
    }

    final List awaitingTasks = threadPool.shutdownNow();
}

In this example, we're submitting 100 tasks to the thread pool. When we invoke shutdownNow, it returns the tasks that aren't started yet. Then it tries to cancel the running tasks.

When we run the application:

Doing work.
Doing work.
...
Pool is terminating.
Doing work.
Pool is terminating.
Doing work.
Tasks that didn't start: 84

We can see that we have 84 tasks that didn't start. Moreover, already running tasks complete normally, suggesting that the cancellation has no effect. ThreadPoolExecutor, which newFixedThreadPool returns, uses Thread.interrupt as the cancellation mechanism. Thus if a task isn't responsive to interrupts, it may never stop and may prevent the pool termination. In our example, the task isn't responsive to interrupts but also not blocking. So the pool terminates successfully.

Let's see another task that contains a blocking operation:

public void shutdownNowTheBlocker() throws InterruptedException {
    final ExecutorService threadPool = Executors.newFixedThreadPool(5);
    final BlockingQueue values = new LinkedBlockingQueue<>();

    threadPool.submit(() -> {
        return values.take();
    });

    threadPool.shutdownNow();
}

In this example, the task blocks when it invokes BlockingQueue.take. Fortunately, this queue method is responsive to interrupts. So when we request a shutdown invoking shutdownNow, the task stops its execution and the pool terminates successfully.

Next, we'll examine a task that isn't responsive to interrupts and also contains a loop:

public void shutdownNowTheLooper() throws InterruptedException {
    final ExecutorService threadPool = Executors.newFixedThreadPool(5);
    threadPool.execute(() -> {
        while (true) {
            doSomething();
        }
    });

    threadPool.shutdownNow();
}

Here, although the pool interrupts its threads, our task doesn't stop. For shutdown and cancellation to work properly, the task must be cooperative and check the interruption status and exit immediately if it is set. Unfortunately, our task loops forever.

4. Two-Phased Termination

Now that we have seen two approaches to shut down a thread pool, we'll now combine them. In fact, we'll use the approach outlined in the ExecutorService Javadoc:

public void shutdownAndAwaitTermination(int n) throws InterruptedException {
    final ExecutorService threadPool = Executors.newFixedThreadPool(1);
    
    threadPool.shutdown(); // Disable new tasks from being submitted
    if (!threadPool.awaitTermination(n, TimeUnit.SECONDS)) {
        threadPool.shutdownNow(); // Cancel currently executing tasks
        if (!threadPool.awaitTermination(n, TimeUnit.SECONDS)) {
            System.out.println("The pool did not terminate");
        }
    }
}

Here, we're first calling shutdown and then waiting for n seconds. The awaitTermination call will block until the pool terminates or the timeout occurs. Then we're invoking shutdownNow to cancel any remaining tasks. Then we're waiting for additional n seconds. If the pool doesn't terminate after these steps, we must inspect and modify the cancellation policies for our tasks.

5. Summary

In this tutorial, we've examined the ways to terminate a thread pool using ExecutorService. We investigated the shutdown and shutdownNow methods. Lastly, we used these two methods to create a versatile termination method.

As always, the source code for all examples in this tutorial is available on Github.