Hasty Briefsbeta

双语

How to deadlock a Java ExecutorService

10 days ago
  • #Java Concurrency
  • #ExecutorService Deadlock
  • #ForkJoinPool
  • 在Java中,当一个具有有限线程数的ExecutorService上的任务同步等待同一执行器上的另一个任务完成时,由于线程耗尽导致第二个任务无法启动,从而可能发生死锁。
  • 最小化复现涉及一个单线程执行器,其中任务S调度任务T并通过CompletableFuture.join()进行等待,导致两者无限期阻塞;修复方法包括增加线程数、使用异步等待或切换到虚拟线程。
  • 如果有足够多的任务同步等待依赖任务完成,即使在多线程环境下也会发生死锁,例如一个大小为2的固定线程池中,两个任务对相互阻塞。
  • 虚拟线程(例如,通过Executors.newVirtualThreadPerTaskExecutor()创建)通过允许大量轻量级线程而不引起资源耗尽来避免死锁,但在不支持JEP 491的较旧JDK版本上需要谨慎使用。
  • 来自more-log4j2的真实案例展示了死锁现象:当AsyncHttpAppender因阻塞而过载并使用ForkJoinPool.commonPool时,日志记录任务阻塞线程,阻止了缓冲区清空所需的信号量释放。
  • HttpClient通过whenCompleteAsync使用公共ForkJoinPool来确保依赖的完成不会让执行器资源耗尽,但如果该池被阻塞任务饱和,则会加剧死锁。
  • 建议:通过使用专用执行器避免过度使用公共ForkJoinPool,考虑虚拟线程以提高可伸缩性,并解决同一执行器内同步等待这一根本原因。