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.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
Why Does DiscardPolicy Cause invokeAll to Hang? Uncovering a JDK Thread‑Pool Bug

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

invokeAll

wraps 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).

JavaconcurrencyThreadPoolbugExecutorServiceDiscardPolicyinvokeAll
Su San Talks Tech
Written by

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.

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.