shutdown() is a graceful stop — it stops accepting new tasks but lets already-submitted ones finish. shutdownNow() is an abrupt stop — it interrupts running tasks and abandons the queue. Neither blocks; awaitTermination() is the separate call that actually waits for the pool to drain.
The three methods
- shutdown() — transitions the pool to SHUTDOWN. New submissions are rejected (
RejectedExecutionException), but queued and in-flight tasks run to completion. Non-blocking; returns immediately. - shutdownNow() — transitions to STOP. Drains and returns the queued-but-not-started tasks as a
List<Runnable>, and callsThread.interrupt()on every worker. Tasks only actually stop if their code is responsive to interruption. - awaitTermination(timeout, unit) — blocks until all tasks finish, the timeout elapses, or the current thread is interrupted. Returns
trueif the pool terminated within the window.
The correct shutdown idiom
void shutdownGracefully(ExecutorService pool) {
pool.shutdown(); // stop taking new work
try {
if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
List<Runnable> dropped = pool.shutdownNow(); // force interrupt
log.warn("Forcing shutdown, {} tasks never started", dropped.size());
if (!pool.awaitTermination(10, TimeUnit.SECONDS)) {
log.error("Pool did not terminate");
}
}
} catch (InterruptedException e) {
pool.shutdownNow();
Thread.currentThread().interrupt(); // restore the flag
}
}
The pattern is: request graceful shutdown, wait a bounded time, escalate to shutdownNow(), then wait again.
On Java 19+, ExecutorService implements AutoCloseable, so try-with-resources calls close(), which is effectively shutdown() plus awaitTermination until done — convenient for short-lived pools.