Executors & Thread Pools: What Actually Happens to Queued and Interrupted Tasks

Part 1 of 8 in Java Concurrency: Deep Dive

ExecutorService hands tasks to a pool of threads instead of you managing Thread objects directly. This post runs two small Java programs, each compiled and run for real, to show what a fixed pool actually does with more tasks than threads, and what its two shutdown methods actually do to tasks that are running or still queued.

More tasks than threads: watching them queue

QueuingDemo creates Executors.newFixedThreadPool(2) and submits 5 tasks, each sleeping 500ms and printing when it starts and finishes relative to program start.

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

public class QueuingDemo {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        long start = System.nanoTime();

        for (int i = 1; i <= 5; i++) {
            final int taskId = i;
            pool.submit(() -> {
                long startedAtMs = (System.nanoTime() - start) / 1_000_000;
                System.out.println("task " + taskId + " started at " + startedAtMs + "ms on " + Thread.currentThread().getName());
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                long finishedAtMs = (System.nanoTime() - start) / 1_000_000;
                System.out.println("task " + taskId + " finished at " + finishedAtMs + "ms on " + Thread.currentThread().getName());
            });
        }

        pool.shutdown();
        pool.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println("all tasks done");
    }
}

Real javac+java output, JDK 25 (Zulu):

task 1 started at 5ms on pool-1-thread-1
task 2 started at 5ms on pool-1-thread-2
task 1 finished at 512ms on pool-1-thread-1
task 2 finished at 512ms on pool-1-thread-2
task 3 started at 513ms on pool-1-thread-1
task 4 started at 513ms on pool-1-thread-2
task 3 finished at 1013ms on pool-1-thread-1
task 4 finished at 1013ms on pool-1-thread-2
task 5 started at 1014ms on pool-1-thread-1
task 5 finished at 1514ms on pool-1-thread-1
all tasks done

Only tasks 1 and 2 started at 5ms — the pool’s two threads. Tasks 3 and 4 didn’t start until 513ms, right after tasks 1 and 2 finished, and task 5 didn’t start until 1014ms, after 3 and 4 finished. With 2 threads and 5 tasks at 500ms each, the run took about 1514ms total instead of running all 5 at once.

shutdown() vs shutdownNow()

ShutdownVsShutdownNowDemo runs the same setup twice against a single-threaded pool: submit a task that sleeps 1000ms (the “running task”), immediately submit a second task behind it (the “queued task”), wait 100ms so the running task has actually started, then call either shutdown() or shutdownNow().

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

public class ShutdownVsShutdownNowDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("--- shutdown() ---");
        runShutdown();

        System.out.println();
        System.out.println("--- shutdownNow() ---");
        runShutdownNow();
    }

    static void runShutdown() throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(1);

        pool.submit(() -> {
            System.out.println("running task: started");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("running task: interrupted");
                Thread.currentThread().interrupt();
                return;
            }
            System.out.println("running task: finished normally");
        });

        pool.submit(() -> {
            System.out.println("queued task: started");
        });

        Thread.sleep(100); // let the running task actually start first
        pool.shutdown();
        pool.awaitTermination(5, TimeUnit.SECONDS);
        System.out.println("shutdown(): pool terminated, isTerminated=" + pool.isTerminated());
    }

    static void runShutdownNow() throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(1);

        pool.submit(() -> {
            System.out.println("running task: started");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("running task: interrupted");
                Thread.currentThread().interrupt();
                return;
            }
            System.out.println("running task: finished normally");
        });

        pool.submit(() -> {
            System.out.println("queued task: started");
        });

        Thread.sleep(100); // let the running task actually start first
        var neverStarted = pool.shutdownNow();
        pool.awaitTermination(5, TimeUnit.SECONDS);
        System.out.println("shutdownNow(): " + neverStarted.size() + " task(s) never started, isTerminated=" + pool.isTerminated());
    }
}

Real javac+java output, JDK 25 (Zulu):

--- shutdown() ---
running task: started
running task: finished normally
queued task: started
shutdown(): pool terminated, isTerminated=true

--- shutdownNow() ---
running task: started
running task: interrupted
shutdownNow(): 1 task(s) never started, isTerminated=true

Under shutdown(), the running task printed “finished normally” and the queued task still ran afterward, printing “queued task: started”. Under shutdownNow(), the running task caught an InterruptedException and printed “interrupted” instead of finishing, and the queued task never ran at all — shutdownNow() returned a list with 1 task in it, the one it pulled off the queue before it could start.

Takeaway

The same setup — one running task, one queued task — played out differently depending on which shutdown method was called: shutdown() let both the running task finish normally and the queued task start and finish, while shutdownNow() interrupted the running task mid-sleep and returned the queued task in its “never started” list instead of running it.