Why Does Executors.newSingleThreadExecutor Shut Down Unexpectedly? Uncovering GC and Finalize Mysteries
This article investigates the intermittent RejectedExecutionException caused by Executors.newSingleThreadExecutor, explains how Java's garbage collector and finalize method can prematurely shut down thread pools, demonstrates the behavior with reproducible code, and shows how newer JDK versions have fixed the issue.
Problem Description
While troubleshooting an occasional thread‑pool error in production, the following exception was observed:
java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@a5acd19 rejected from java.util.concurrent.ThreadPoolExecutor@30890a38[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]The reproduced code creates a single‑thread executor via Executors.newSingleThreadExecutor() and submits a task that returns a Future result.
public class ThreadPoolTest {
public static void main(String[] args) {
ThreadPoolTest threadPoolTest = new ThreadPoolTest();
for (int i = 0; i < 8; i++) {
new Thread(() -> {
while (true) {
Future<String> future = threadPoolTest.submit();
try {
String s = future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}).start();
}
// Simulate GC pressure
new Thread(() -> {
while (true) {
System.gc();
}
}).start();
}
/** Asynchronous task */
public Future<String> submit() {
ExecutorService executorService = Executors.newSingleThreadExecutor();
FutureTask<String> futureTask = new FutureTask<>(() -> {
Thread.sleep(50);
return System.currentTimeMillis() + "";
});
executorService.execute(futureTask);
return futureTask;
}
}Analysis & Questions
The first question is why the thread pool is shut down even though the code never calls shutdown(). The source of Executors.newSingleThreadExecutor creates a FinalizableDelegatedExecutorService, whose finalize() method invokes shutdown() before the object is garbage‑collected.
GC only reclaims unreachable objects, but the JIT compiler may nullify variables that are no longer used, making them unreachable earlier than the stack frame ends. This can cause finalize() to run while the method is still active.
A reachable object is any object that can be accessed in any potential continuing computation from any live thread. Optimizing transformations may set a variable to null earlier, allowing the object to become reclaimable sooner.
Experiments show that if an object is set to null or a thread switch occurs, the object can be finalized early. Adding a final reference (e.g., printing the object) prevents early finalization.
Conclusion
Even though GC only collects unreachable objects, compiler/JIT optimizations can make objects unreachable earlier, causing premature finalization. Relying on finalize() for important cleanup (as in Executors.newSingleThreadExecutor) is therefore unsafe and was fixed in JDK 11 by adding a reachability fence.
References
https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope
https://bugs.openjdk.java.net/browse/JDK-8145304
https://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.6.1
https://docs.oracle.com/javase/specs/jls/se8/html/jls-6.html#jls-6.3
https://stackoverflow.com/questions/58714980/rejectedexecutionexception-inside-single-executor-service
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
