Q38 of 40 · Core Java

How would you safely shut down an ExecutorService with in-flight tasks?

Core JavaSeniorexecutorserviceshutdownthreadingconcurrencyjavainterruption

Short answer

Short answer: A safe two-phase shutdown calls shutdown() to stop accepting new tasks, then awaits completion with awaitTermination(). If the timeout expires and tasks are still running, call shutdownNow() which interrupts running tasks. Always handle InterruptedException and restore the interrupt flag. The standard pattern is codified in the Javadoc for ExecutorService.

Detail

import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * Graceful two-phase shutdown from ExecutorService Javadoc.
 */
void shutdownGracefully(ExecutorService executor) {
    executor.shutdown();  // phase 1: reject new tasks, let queue drain

    try {
        // Phase 2: wait up to 30 s for in-flight tasks to finish
        if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
            executor.shutdownNow();  // phase 3: interrupt running tasks

            // Wait again for interrupted tasks to respond and exit
            if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
                System.err.println("Executor did not terminate");
            }
        }
    } catch (InterruptedException e) {
        // Current thread was interrupted while waiting — cancel remaining tasks
        executor.shutdownNow();
        Thread.currentThread().interrupt();  // restore interrupt status
    }
}

Task-side: responding to interruption

import java.util.List;

// Tasks must cooperate with interruption — check interrupted flag in loops
Runnable testTask = () -> {
    for (var testCase : testCases) {
        if (Thread.currentThread().isInterrupted()) {
            System.out.println("Interrupted — aborting remaining tests");
            return;
        }
        runTest(testCase);
    }
};

JUnit / automation teardown integration

import org.junit.jupiter.api.AfterAll;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class ParallelApiTest {
    private static final ExecutorService POOL =
        Executors.newFixedThreadPool(10);

    @AfterAll
    static void teardown() {
        shutdownGracefully(POOL);
    }
}

try-with-resources (Java 19+ AutoCloseable)

// ExecutorService implements AutoCloseable in Java 19+
// close() = shutdown() + awaitTermination(Long.MAX_VALUE) + handle interrupt
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (var task : tasks) executor.submit(task);
} // auto-shutdown and await when block exits

Key rules

  1. Always call shutdown() before awaitTermination() — never skip phase 1.
  2. After shutdownNow(), check returned list of un-started tasks if you need to log them.
  3. Restore interrupt status after catching InterruptedException — don't swallow it.

// WHAT INTERVIEWERS LOOK FOR

The two-phase shutdown pattern (shutdown → awaitTermination → shutdownNow → awaitTermination again). The difference between shutdown() (soft stop) and shutdownNow() (interrupts). Restoring the interrupt flag after catching InterruptedException. Senior candidates mention task-side cooperation via isInterrupted() and the Java 19+ AutoCloseable executor.

// COMMON PITFALL

Calling shutdownNow() immediately without giving tasks a chance to finish, or swallowing InterruptedException without restoring Thread.currentThread().interrupt(). Swallowing the interrupt status prevents upstream code from knowing the thread was interrupted.