当使用单线程执行器时,为什么"header.get() + footer.get()"会导致死锁?

4

这是《Java并发编程实践》中的8.1列表:

public class ThreadDeadlock  {
   ExecutorService exec = Executors.newSingleThreadExecutor();

   public class RenderPageTask implements Callable<String>  {
      public String call() throws Exception  {
         Future<String> header, footer;
         header = exec.submit(new LoadFileTask("header.html"));
         footer = exec.submit(new LoadFileTask("footer.html"));
         String page = renderBody();

         //Will deadlock -- task waiting for result of subtask
         return header.get() + page + footer.get();
      }
   }
}

这在第8章:线程池 > 第8.1.1节 线程饥饿死锁 中提到,并有标题:

"任务在单线程 Executor 中发生死锁。 不要这样做。"

为什么会导致死锁?我认为应该先调用header.get(),然后调用footer.get(),每个结果都追加到字符串中。为什么单线程 Executor 不足以一个接着一个地运行它们?

相关章节内容:

8.1.1 线程饥饿死锁

如果依赖于其他任务的任务在线程池中执行,则可能会发生死锁。在单线程执行器中,提交另一个任务到同一执行器并等待其结果的任务将始终死锁。第二个任务坐在工作队列上,直到第一个任务完成,但是第一个任务不会完成,因为它正在等待第二个任务的结果。如果所有线程都在执行被阻塞等待其他仍在工作队列中的任务的任务,则较大的线程池中也可能发生这种情况。这称为线程饥饿死锁,并且只要池任务启动了对某些资源或条件的无限制阻塞等待,该资源或条件只能通过另一个池任务的操作成功,例如等待另一个任务的返回值或副作用,除非您可以保证池足够大。

清单8.1中的ThreadDeadlock说明了线程饥饿死锁。 RenderPageTask 提交两个附加任务以获取页面页眉和页脚,渲染页面正文,等待页眉和页脚任务的结果,然后将页眉、正文和页脚组合成最终页面。在单线程执行器中,ThreadDeadlock将始终死锁。同样,如果池不够大,使用屏障协调彼此的任务也可能导致线程饥饿死锁。


我没有看到任何死锁。LoadFileTask 做什么? - Sotirios Delimanolis
这本书是免费的吗?我似乎无法阅读你所提到的部分,而不需要支付大约25美元。此外,我们在这里缺少一些代码,例如,什么是LoadFileTask?这使得分析问题或解释为什么会死锁变得困难。虽然这段代码片段不包含任何可执行内容,因此没有任何东西可以死锁,因为没有任何东西可以运行。 - RDM
2
这本书是否说RenderPageTask本身已经在同一个执行器中运行?那确实会死锁... - GPI
没有可展示的代码。 LoadFileTask 是一个虚构任务。我将添加这一章节的文本... - aliteralmind
在单线程执行器中,一个任务提交另一个任务到同一个执行器并等待其结果将会导致死锁。第二个任务会一直等待在工作队列中,直到第一个任务完成,但是第一个任务不会完成,因为它正在等待第二个任务的结果。 - Sotirios Delimanolis
2个回答

7
实际死锁将在一个RenderPageTask实例被提交到相同的执行者实例并提交其任务时发生。
例如,添加:
exec.submit(new RenderPageTask());

如果您不小心将RenderPageTask提交给错误的执行器实例,那么您可能会遇到死锁问题。

当然,这可以被视为周围代码的问题(即,您可以简单地定义并记录您的RenderPageTask不应该提交给此执行器实例),但良好的设计将完全避免这种陷阱。

一种可能的解决方案是使用ForkJoinPool,它使用工作窃取来避免这种形式的潜在死锁。


您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - RDM
顺便提一下,这就是为什么我更喜欢使用委托而不是使用Future对象和线程阻塞进行工作的原因。 - RDM
我没有看到RenderPageTask在执行器中被提交的地方。代码中唯一的submit是用于头部和尾部子任务。我理解如果整个"render"任务被提交给执行器,在单线程池中会发生死锁,因为它正在等待其子任务...而这些子任务需要该线程来工作。 - aliteralmind
我明白了。RenderPageTask 是一个 Callable。为了执行它,必须将它提交到一些我们看不到的其他代码中的执行器中。这个问题被标记为重复的问题就很清楚地表明了这一点。 - aliteralmind

3

我猜测RenderPageTask已经提交到与其他任务相同的执行池中,因此在RenderPageTask完成之前,其他任务将不会启动- 但是这永远不会发生- 我们陷入了死锁。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接