Hasty Briefsbeta

Bilingual

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.