Avoiding Deadlocks When Using Thread Pools with Parent-Child Tasks

When working with thread pools, a common pitfall arises when parent and child tasks share the same pool. This can lead to unexpected deadlocks and incorrect execution timing.

Consider a simple example where a thread pool handles five independent tasks via a loop. Each task processes data asynchronously and uses a CountDownLatch to signal completion:

ExecutorService executor = Executors.newFixedThreadPool(3);
CountDownLatch latch = new CountDownLatch(5);

for (int i = 0; i < 5; i++) {
    final int taskId = i;
    executor.submit(() -> {
        try {
            // Simulate work
            Thread.sleep(1000);
            System.out.println("Task " + taskId + " completed");
        } finally {
            latch.countDown();
        }
    });
}

latch.await();
System.out.println("All tasks finished");

This setup works well for straightforward cases. However, if we introduce asynchronous processing inside each task — where subtasks also use the same thread pool — the behavior changes dramatically.

Here's an altered version:

for (int i = 0; i < 5; i++) {
    final int taskId = i;
    executor.submit(() -> {
        CountDownLatch subLatch = new CountDownLatch(2);
        
        // Submit subtasks
        for (int j = 0; j < 2; j++) {
            executor.submit(() -> {
                try {
                    Thread.sleep(500);
                    System.out.println("Processing data for task " + taskId + " part " + j);
                } finally {
                    subLatch.countDown();
                }
            });
        }
        
        try {
            subLatch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        System.out.println("Task " + taskId + " completed");
        latch.countDown();
    });
}

latch.await();
System.out.println("All tasks finished");

In this case, the main thread waits for all parent tasks to complete. But since these parent tasks themselves spawn additional tasks into the same pool, a deadlock occurs. The pool’s threads are occupied by the parent tasks waiting for their children to finish, while those children are queued and never executed.

The problem becomes evident when observing execution logs. Although all parent tasks appear to finish quickly, the actual computation continues in the background. This results in misleading performance metrics.

To fix this issue, isolate the parant and child tasks by using separate thread pools:

ExecutorService parentPool = Executors.newFixedThreadPool(3);
ExecutorService childPool = Executors.newFixedThreadPool(3);

// Use parentPool for parent tasks
parentPool.submit(() -> {
    CountDownLatch subLatch = new CountDownLatch(2);
    
    for (int j = 0; j < 2; j++) {
        childPool.submit(() -> {
            try {
                Thread.sleep(500);
                System.out.println("Processing data for task " + taskId + " part " + j);
            } finally {
                subLatch.countDown();
            }
        });
    }
    
    try {
        subLatch.await();
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    
    System.out.println("Parent task completed");
});

By separating concerns into distinct thread pools, both parent and child operations execute independently, avoiding resource contention and ensuring correct scheduling.

This pattern is especially critical in distributed systems where microservices interact through shared thread pools. If multiple service endpoints rely on a single thread pool for asynchronous operations, they may block eachother due to mutual dependency.

Key takeaway:

Avoid sharing thread pools between parent and child tasks. Doing so risks deadlocks caused by thread starvation when child tasks are queued but cannot execute.

Tags: Thread Pool deadlock Concurrency java multithreading

Posted on Sun, 30 Aug 2026 16:41:36 +0000 by czs