Not all concurrency failures come from incorrect locking.

Sometimes the bug is simply that threads, pools, or schedulers are created repeatedly and never shut down.

That is thread leakage or executor leakage.


Problem Statement

Suppose each request creates its own executor for background work.

If those executors are not shut down reliably, the service accumulates threads over time.

This turns into:

  • memory pressure
  • scheduler overhead
  • higher context-switch cost
  • broken shutdown behavior

Naive Version

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

class ReportService {
    void generateReport() {
        ExecutorService executor = Executors.newFixedThreadPool(4);
        executor.submit(() -> writeReport());
    }

    void writeReport() {
    }
}

This leaks executor lifecycle ownership.

The method creates a pool, submits work, and then forgets about it.


Runnable Example

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorLeakDemo {

    public static void main(String[] args) {
        List<ExecutorService> leakedExecutors = new ArrayList<>();

        for (int i = 0; i < 50; i++) {
            ExecutorService executor = Executors.newSingleThreadExecutor();
            leakedExecutors.add(executor);
            executor.submit(() -> {
                try {
                    Thread.sleep(10_000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        System.out.println("Created executors = " + leakedExecutors.size());
        System.out.println("Active threads may continue long after useful work ownership is lost");
    }
}

This demo intentionally leaks references so the process impact is obvious. Real systems often do it more indirectly through helper classes and forgotten shutdown hooks.


Production-Style Scenarios

Leakage appears in code like:

  • creating a new pool per request, tenant, or batch item
  • scheduled executors started during refresh cycles and never stopped
  • test helpers or utility classes hiding internal executors
  • libraries creating background threads without clear lifecycle management

Symptoms include:

  • thread count creeping upward
  • pods needing restart after long uptime
  • shutdown hanging because non-daemon workers remain alive
  • unexpected CPU burn from thousands of mostly idle threads

Better Design

Executors should usually be:

  • long-lived
  • owned by the application lifecycle
  • explicitly shut down
  • monitored as resources, not treated as throwaway helpers

For example:

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

class SafeReportService implements AutoCloseable {
    private final ExecutorService executor = Executors.newFixedThreadPool(4);

    void generateReport() {
        executor.submit(this::writeReport);
    }

    void writeReport() {
    }

    @Override
    public void close() throws Exception {
        executor.shutdown();
        if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
            executor.shutdownNow();
        }
    }
}

This makes lifecycle ownership explicit.


Diagnostics

Useful signals:

  • thread dumps showing ever-growing pool names
  • metrics for live threads, active threads, and queue sizes
  • shutdown logs showing executors never terminating
  • heap analysis showing retained ExecutorService instances

Naming threads and pools consistently matters here just as much as in deadlock debugging.


Ownership Rule

A simple rule prevents many leakage bugs: who creates the executor must define its lifecycle. If ownership is unclear, shutdown will eventually be unclear too. That is why hidden internal pools inside helpers and utility classes are so dangerous. They create concurrency resources without making anyone responsible for:

  • shutdown ordering
  • queue draining
  • rejected work policy
  • thread naming and monitoring

Treating executors as application infrastructure instead of convenient helper objects is one of the healthiest architectural upgrades a service team can make.

Testing and Review Notes

Leakage bugs often hide until long-running integration or soak tests. Add checks around thread-count growth, pool shutdown, and repeated create-destroy cycles. A service that looks fine after one request can still leak badly after ten thousand.

Key Takeaways

  • Threads and executors are resources with lifecycle, not disposable implementation details.
  • Repeated creation without shutdown leads to thread leakage, memory pressure, and unstable shutdown behavior.
  • Prefer shared application-owned executors with explicit close logic.
  • Monitor thread count and executor growth before leakage becomes an outage.

Next post: False Sharing and Cache Line Contention in Java

Comments