Deadlock with Java ThreadPoolExecutor

#java #executor

Table of Contents

Introduction

Recently, I was working with Java Executor and wrote a complete program and expected it to work. But it didn't, obviously, hence this post.

Setup

There is a hierarchy in the way work is distributed. There is a Parent task, which will trigger multiple Child tasks and wait for their completion. We want to execute the Child tasks in parallel and also the Parent tasks in parallel. So, my first attempt was to use Java's Executor. To keep things simple, the Parent task will simply create a single Child task.

Parent Task

The Parent Task is straight forward. It creates a Child task with a latch, to track the completion.

 1static class ParentTask implements Runnable {
 2    private final CountDownLatch latch;
 3    private final Executor executor;
 4
 5    ParentTask(final CountDownLatch latch, final Executor executor) {
 6        this.latch = latch;
 7        this.executor = executor;
 8    }
 9
10    @Override
11    public void run() {
12        print("ParentTask");
13        final CountDownLatch childTaskLatch = new CountDownLatch(1);
14        executor.execute(new ChildTask(childTaskLatch));
15        await(childTaskLatch);
16        latch.countDown();
17    }
18}

Child Task

Child task will simply get the latch and count it down. Nothing more, nothing less.

 1static class ChildTask implements Runnable {
 2    private final CountDownLatch latch;
 3
 4    public ChildTask(final CountDownLatch latch) {
 5        this.latch = latch;
 6    }
 7
 8    @Override
 9    public void run() {
10        latch.countDown();
11        print("ChildTask");
12    }
13}

Main

The Main will simply create the Executor and trigger Parent tasks.

 1public static void main(String[] args) {
 2    final int CORE_POOL_SIZE = 1;
 3    final int MAX_POOL_SIZE = 5;
 4    final long KEEP_ALIVE_TIME_IN_MS = 1000;
 5    final int MAX_CAPACITY = 100;
 6    final ThreadPoolExecutor executor = new ThreadPoolExecutor(
 7            CORE_POOL_SIZE,
 8            MAX_POOL_SIZE,
 9            KEEP_ALIVE_TIME_IN_MS, TimeUnit.MILLISECONDS,
10            new LinkedBlockingQueue<>(MAX_CAPACITY)
11    );
12    final CountDownLatch latch = new CountDownLatch(1);
13    executor.execute(new ParentTask(latch, executor));
14    await(latch);
15    executor.shutdownNow();
16}

Helpers

Following are the helper functions used.

 1static void print(final String msg) {
 2    System.out.printf("[%s] [%s]\n", Thread.currentThread().getName(), msg);
 3}
 4
 5static void await(final CountDownLatch latch) {
 6    try {
 7        latch.await();
 8    } catch (InterruptedException e) {
 9        throw new RuntimeException(e);
10    }
11}

Result

Expectation

I expected this code to execute the Parent task in the Executor and as the MAX_POOL_SIZE is 5, I expected the Child task as well to be executed by the Executor with the following in the Console.

1[pool-1-thread-1] [ParentTask]
2[pool-1-thread-2] [ChildTask]

Actual Output

1[pool-1-thread-1] [ParentTask]
2...Program Hangs...

Explanation

This is because of a deadlock. Both the Parent and Child tasks share the Executor. The CORE_POOL_SIZE is set as 1. So, bare minimum one thread will be there. When the Parent task is executed with the Executor, it will use the core thread that is already present. The Parent task executes the Child task as well, with the same Executor. As the Core Pool Size (1) is lesser than the Maximum Pool size (5), I expected a new Thread to be created when the Child task is executed with Executor. But the actual behaviour of the ThreadPoolExecutor is documented as follows in java.util.concurrent.ThreadPoolExecutor

If corePoolSize or more threads are running, the Executor always prefers queuing a request rather than adding a new thread.

So, as the Parent task is already being executed by the Core Thread, the Child Task is Queued and waiting for the Parent task to be completed, but the Parent task is waiting for the Child task to complete. Hence the deadlock.

Finishing Thoughts

Although increasing the number of core threads in the Main class would get the program work as expected, that may not be the right way to solve this problem. I believe a better option would be to have a dedicated Executor for the Parent tasks, and a dedicated Executor for the Child tasks.