您几乎要回答自己的问题。解决方案是声明“
ForkJoinPool通过从
join()调用内部的其他线程中窃取工作来避免此问题”。只要线程由于某些其他原因而被阻塞(除外)
ForkJoinPool.join(),就不会发生窃取工作的情况,线程只是等待而什么也不做。
原因是在Java中,无法
ForkJoinPool阻止其线程阻塞,而是给它们提供其他处理方法。线程 本身
需要避免阻塞,而是要求池进行应做的工作。并且这仅在该
ForkJoinTask.join()方法中实现,而不能在任何其他阻塞方法中实现。如果在
Future内部使用
ForkJoinPool,您还将看到饥饿死锁。
为什么仅
ForkJoinTask.join()在Java
API中的任何其他阻止方法中而不是在其他方法中实现工作窃取?嗯,有很多这样的阻塞方法(
Object.wait(),
Future.get()中的任何并发原语
java.util.concurrent,I
/
O方法等),它们与无关
ForkJoinPool,后者只是API中的任意类,因此在所有情况下都添加了特殊情况这些方法将是不好的设计。这也可能导致非常令人惊讶和不希望的后果。假设有一个用户将一个任务传递给一个
ExecutorService等待一个的
Future,然后发现该任务挂起的时间很长,
Future.get()原因是正在运行的线程偷走了其他一些(长时间运行的)工作项,而不是等待
Future并在结果可用后立即继续。一旦线程开始处理另一个任务,它就不能返回到原始任务,直到第二个任务完成为止。因此,其他阻止方法不进行工作窃取实际上是一件好事。对于a
ForkJoinTask,这个问题不存在,因为不重要的是尽快继续执行主要任务,因此重要的是尽可能高效地共同处理所有任务。
也无法实现您自己的方法来在内进行偷窃工作
ForkJoinPool,因为所有相关部分都不公开。
但是,实际上还有第二种方法可以防止饥饿死锁。这称为 托管阻止
。它不使用工作窃取(以避免上面提到的问题),但是还需要将要阻塞的线程与线程池进行积极协作。使用托管阻塞,线程告诉线程池它可能在被阻塞 之前
它调用潜在的阻塞方法,并在阻塞方法完成时通知池。然后,线程池知道存在饥饿死锁的风险,并且如果其所有线程当前都处于某个阻塞操作中并且还有其他任务要执行,则可能会产生其他线程。请注意,由于附加线程的开销,这效率不如工作窃取。如果您使用普通期货和托管分块来实现递归并行算法,而不是使用
ForkJoinTask加上工作偷窃,额外线程的数量可能会非常大(因为在算法的“划分”阶段,将创建许多任务并将其分配给立即阻塞并等待子任务结果的线程)。但是,仍然可以防止出现饥饿死锁,并且避免了一个任务必须等待很长时间的问题,因为该任务的线程同时开始处理另一个任务。
在
ForkJoinPoolJava的支持托管阻塞。为了使用它,需要实现一个接口
ForkJoinPool.ManagedBlocker,以便从
block该接口的方法中调用任务要执行的潜在阻塞方法。然后,任务可能不会直接调用阻塞方法,而是需要调用static方法
ForkJoinPool.managedBlock(ManagedBlocker)。此方法在阻塞之前和之后处理与线程池的通信。如果当前任务未在内执行
ForkJoinPool,那么它也可以工作,它仅调用阻塞方法。
我在Java API(适用于Java 7)中找到的唯一实际使用托管阻塞的地方是class
Phaser。(此类是像互斥锁和闩锁一样的同步屏障,但更灵活,更强大。)因此,与任务
Phaser内部同步
ForkJoinPool时应使用托管阻塞,并且可以避免饥饿死锁(但
ForkJoinTask.join()仍可取,因为它使用工作窃取而不是托管阻塞)
。无论您是
ForkJoinPool直接使用还是通过其
ExecutorService界面使用,此方法都有效。但是,如果您使用其他任何
ExecutorService类(例如由类创建的类)
Executors,则将无法使用,因为它们不支持托管阻塞。
在Scala中,托管阻塞的使用更加广泛(description,API)。



