Deadlock with Java ThreadPoolExecutor
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.
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.
Result
Expectation
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.