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.