How to deadlock a Java ExecutorService
9 hours ago
- #Java Concurrency
- #ExecutorService Deadlock
- #ForkJoinPool
- A Java ExecutorService with a bounded number of threads can deadlock when a task running on it synchronously waits for another task on the same executor to complete, preventing the second task from starting due to thread exhaustion.
- Minimal reproduction involves a single-threaded executor where taskS schedules taskT and waits via CompletableFuture.join(), causing both to block indefinitely; fixes include adding threads, using asynchronous waits, or switching to virtual threads.
- Deadlock can occur with multiple threads if enough tasks synchronously wait for dependent tasks, as shown with a fixed thread pool of size 2 and two task pairs blocking each other.
- Virtual threads (e.g., Executors.newVirtualThreadPerTaskExecutor()) avoid deadlock by allowing many lightweight threads without resource exhaustion, though caution is needed on older JDKs without JEP 491 support.
- Real-world example from more-log4j2 shows deadlock when AsyncHttpAppender overloads with blocking and uses ForkJoinPool.commonPool, where logging tasks block threads, preventing semaphore release needed for buffer draining.
- HttpClient uses common ForkJoinPool via whenCompleteAsync to ensure dependent completions don't starve its executor, contributing to deadlock if the pool is saturated by blocking tasks.
- Recommendations: avoid overloading common ForkJoinPool by using dedicated executors, consider virtual threads for scalability, and address synchronous waits within the same executor as the root cause.