Why Your Java ThreadPool Won’t Release Threads – The Hidden Role of shutdown()

This article investigates why a Java application can accumulate hundreds of waiting threads in a thread pool, explains how the lack of shutdown calls prevents garbage collection, and walks through the internal shutdown logic that finally releases both threads and the pool itself.

Java Interview Crash Guide
Java Interview Crash Guide
Java Interview Crash Guide
Why Your Java ThreadPool Won’t Release Threads – The Hidden Role of shutdown()

Today I discovered an application with nearly 1,000 threads, most of them in a waiting state, yet CPU and memory usage remained low. The thread names all started with "pool", indicating a single thread pool was responsible.

Inspecting the code revealed that the pool was created with new FixedThreadPool() instead of the usual Executors.newFixedThreadPool(), which meant the default thread names were used and the pool was never shut down.

To reproduce the problem I wrote a demo that repeatedly creates a fixed thread pool without calling shutdown(). Using Java VisualVM I observed that both the thread count and the pool instances kept growing.

When I added a call to shutdown() at the end of the method, the threads and the pool were reclaimed, confirming that proper shutdown is required for garbage collection.

The reason lies in the JVM’s garbage‑collection roots: a running thread (including one in WAITING or TIME_WAITING) is a GC root, so as long as the thread object remains reachable, the thread‑pool object cannot be collected.

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(SHUTDOWN);
        interruptIdleWorkers();
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}

private void interruptIdleWorkers() {
    interruptIdleWorkers(false);
}

private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers) {
            Thread t = w.thread;
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {}
                finally {
                    w.unlock();
                }
            }
            if (onlyOne) break;
        }
    } finally {
        mainLock.unlock();
    }
}

The interruptIdleWorkers method iterates over all worker threads and calls interrupt(), which forces waiting threads to throw an InterruptedException.

Inside the worker’s runWorker method, after a task finishes or an exception occurs, processWorkerExit removes the worker from the workers set, breaking the GC‑root chain.

private void processWorkerExit(Worker w, boolean completedAbruptly) {
    if (completedAbruptly)
        decrementWorkerCount();
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        completedTaskCount += w.completedTasks;
        workers.remove(w);
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    // possibly add a replacement worker
}

When all workers are removed and the surrounding request thread ends, the thread‑pool object itself becomes unreachable and is also garbage‑collected.

A running thread is considered a so‑called garbage‑collection root and is one of those things keeping stuff from being garbage collected.

In summary:

The shutdown() or shutdownNow() method interrupts idle workers, causing waiting threads to exit.

Exited workers are removed from the pool’s internal collection, breaking the GC‑root link.

Once the worker set is empty and no external references remain, both the threads and the thread‑pool object are reclaimed.

Therefore, whenever a thread pool is used locally and not managed as a Spring bean, remember to invoke shutdown() or shutdownNow() to avoid thread and pool leakage.

JavaThreadPoolGarbageCollectionExecutorServiceShutdown
Java Interview Crash Guide
Written by

Java Interview Crash Guide

Dedicated to sharing Java interview Q&A; follow and reply "java" to receive a free premium Java interview guide.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.