当使用单线程Executor时,为什么“header.get()+ footer.get()”会导致死锁?

这是Java Concurrency In Practice中的 8.1列表:

public class ThreadDeadlock { ExecutorService exec = Executors.newSingleThreadExecutor(); public class RenderPageTask implements Callable { public String call() throws Exception { Future 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() ,每个结果都附加到字符串中。 为什么单个线程执行程序不能足以一个接一个地运行这些?

相关章节文字:

8.1.1线程饥饿死锁

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

清单8.1中的ThreadDeadlock说明了线程饥饿死锁。 RenderPageTaskExecutor提交另外两个任务,即获取页眉和页脚,呈现页面正文,等待页眉和页脚任务的结果,然后将页眉,正文和页脚组合到完成的页面中。 使用单线程执行程序, ThreadDeadlock将始终死锁。 同样,如果池不够大,任务与屏障协调的任务也可能导致线程饥饿死锁。

一旦RenderPageTask的实例被提交到提交其任务的同一个执行器实例,就会发生实际的死锁。

例如,添加

 exec.submit(new RenderPageTask()); 

你会遇到僵局。

当然,这可以被认为是周围代码的问题(即,你可以简单地定义和记录你的RenderPageTask不应该被提交给这个执行器实例),但是一个好的设计可以完全避免这样的陷阱。

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

是的,我敢打赌RenderPageTask被提交到与其他任务相同的执行器池,因此在RenderPageTask完成之前,其他任务不会启动 – 但这将永远不会发生 – 我们已经死锁