Why Does DiscardPolicy Cause invokeAll to Hang? Uncovering a JDK Thread‑Pool Bug
An in‑depth analysis reveals how the JDK’s DiscardPolicy rejection strategy can cause invokeAll to block indefinitely, exposing a subtle thread‑pool bug, the official response, and practical code examples demonstrating the issue and ways to avoid it.
Hello, I am Su San. Recently I discovered a bug in the JDK thread pool and dug into its root cause. The JDK team acknowledges it as a bug but treats it as a feature.
JDK Thread‑Pool Rejection Policies
What are the built‑in rejection policies?
AbortPolicy: discards the task and throws RejectedExecutionException (default).
DiscardOldestPolicy: discards the oldest task in the queue and executes the new one.
CallerRunsPolicy: the calling thread runs the task.
DiscardPolicy: silently discards the task without throwing an exception.
The bug I am discussing is triggered by the DiscardPolicy.
What Is the Bug?
The bug link is JDK‑8286463 . It describes a situation where the DiscardPolicy combined with invokerAll can cause the thread pool to block forever.
The description notes that the issue was previously reported and that developers Doug and Martin suggested users could avoid permanent blocking by coding around it.
Another related bug is JDK‑8160037 , which involves shutdownNow. To understand the problem, we first need to examine shutdownNow.
Phenomenon
Using the test case from JDK‑8160037, the original code runs without error:
public class MainTest {
public static void main(String[] args) throws InterruptedException {
List<Callable<Void>> tasks = new ArrayList<>();
for (int i = 0; i < 10; i++) {
int finalI = i;
tasks.add(() -> {
System.out.println("callable " + finalI);
Thread.sleep(500);
return null;
});
}
ExecutorService executor = Executors.newFixedThreadPool(2);
Thread executorInvokerThread = new Thread(() -> {
try {
executor.invokeAll(tasks);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("invokeAll returned");
});
executorInvokerThread.start();
}
}The program finishes normally and prints "invokeAll returned".
After adding a call to shutdownNow (or shutdown) the program still terminates without issue.
However, when we replace the fixed thread pool with a custom pool that uses DiscardPolicy:
public class MainTest {
public static void main(String[] args) throws InterruptedException {
List<Callable<Void>> tasks = new ArrayList<>();
for (int i = 0; i < 10; i++) {
int finalI = i;
tasks.add(() -> {
System.out.println("callable " + finalI);
Thread.sleep(500);
return null;
});
}
ExecutorService executor = new ThreadPoolExecutor(
1,
1,
1,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1),
new ThreadPoolExecutor.DiscardPolicy()
);
Thread executorInvokerThread = new Thread(() -> {
try {
executor.invokeAll(tasks);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("invokeAll returned");
});
executorInvokerThread.start();
Thread.sleep(800);
System.out.println("shutdown");
List<Runnable> runnables = executor.shutdownNow();
for (Runnable r : runnables) {
if (r instanceof Future) ((Future<?>) r).cancel(false);
}
System.out.println("Shutdown complete");
}
}Running this version shows that "invokeAll returned" never appears and the program does not exit, indicating a hang.
Root Cause
The program does not terminate because a non‑daemon thread remains blocked in Future.get() inside invokeAll. The DiscardPolicy silently drops tasks without throwing an exception, so the corresponding Future never completes, causing the invoking thread to wait forever.
Thread‑dump analysis shows the waiting thread’s stack trace points to AbstractExecutorService.invokeAll line 244.
Understanding invokeAll
invokeAllwraps each submitted task into a Future, enqueues it with execute, and then iterates over the list, calling Future.get() on each. If a task is discarded, its Future never finishes, and get() blocks indefinitely.
Effect of DiscardPolicy
When the pool’s queue is full (core = 1, max = 1, queue = 1) and ten tasks are submitted, eight tasks are rejected by DiscardPolicy. Those rejected tasks produce Future objects that remain incomplete, leading to the hang.
Official Response
Martin and Doug acknowledged the behavior, noting that shutdownNow returns a list of tasks that were not started. They suggested handling the returned list, but the real issue is that DiscardPolicy silently drops tasks, leaving their Future s unresolved.
Martin later clarified that the documentation should warn against using DiscardPolicy in real code because it can cause exactly this kind of deadlock.
Conclusion
The bug is not in shutdownNow but in the combination of invokeAll with a rejection policy that silently discards tasks. To avoid the hang, either avoid DiscardPolicy or ensure that all submitted tasks have a reachable completion path (e.g., use AbortPolicy or handle the returned list appropriately).
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.
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.
