Java 我可以使用ForkJoinPool的工作窃取行为来避免线程饥饿死锁吗?
如果池中的所有线程都在等待同一池中排队的任务完成,则正常线程池中会发生线程饥饿死锁Java 我可以使用ForkJoinPool的工作窃取行为来避免线程饥饿死锁吗?,java,multithreading,concurrency,java.util.concurrent,fork-join,Java,Multithreading,Concurrency,Java.util.concurrent,Fork Join,如果池中的所有线程都在等待同一池中排队的任务完成,则正常线程池中会发生线程饥饿死锁ForkJoinPool通过从join()调用内部窃取其他线程的工作,而不是简单地等待,从而避免了此问题。例如: private static class ForkableTask extends RecursiveTask<Integer> { private final CyclicBarrier barrier; ForkableTask(CyclicBarrier barrie
ForkJoinPool
通过从join()
调用内部窃取其他线程的工作,而不是简单地等待,从而避免了此问题。例如:
private static class ForkableTask extends RecursiveTask<Integer> {
private final CyclicBarrier barrier;
ForkableTask(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
protected Integer compute() {
try {
barrier.await();
return 1;
} catch (InterruptedException | BrokenBarrierException e) {
throw new RuntimeException(e);
}
}
}
@Test
public void testForkJoinPool() throws Exception {
final int parallelism = 4;
final ForkJoinPool pool = new ForkJoinPool(parallelism);
final CyclicBarrier barrier = new CyclicBarrier(parallelism);
final List<ForkableTask> forkableTasks = new ArrayList<>(parallelism);
for (int i = 0; i < parallelism; ++i) {
forkableTasks.add(new ForkableTask(barrier));
}
int result = pool.invoke(new RecursiveTask<Integer>() {
@Override
protected Integer compute() {
for (ForkableTask task : forkableTasks) {
task.fork();
}
int result = 0;
for (ForkableTask task : forkableTasks) {
result += task.join();
}
return result;
}
});
assertThat(result, equalTo(parallelism));
}
private static class CallableTask implements Callable<Integer> {
private final CyclicBarrier barrier;
CallableTask(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
public Integer call() throws Exception {
barrier.await();
return 1;
}
}
@Test
public void testWorkStealing() throws Exception {
final int parallelism = 4;
final ExecutorService pool = new ForkJoinPool(parallelism);
final CyclicBarrier barrier = new CyclicBarrier(parallelism);
final List<CallableTask> callableTasks = Collections.nCopies(parallelism, new CallableTask(barrier));
int result = pool.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int result = 0;
// Deadlock in invokeAll(), rather than stealing work
for (Future<Integer> future : pool.invokeAll(callableTasks)) {
result += future.get();
}
return result;
}
}).get();
assertThat(result, equalTo(parallelism));
}
粗略地看一下
ForkJoinPool
的实现,所有常规的ExecutorService
API都是使用ForkJoinTask
实现的,所以我不知道为什么会发生死锁。我知道您在做什么,但我不知道为什么。屏障的概念是,独立的线程可以等待彼此到达一个公共点。你没有独立的线程。线程池(F/J)用于
你正在做一些更适合你的事情
F/J继续的原因是框架创建“继续线程”,以便在所有工作线程都在等待时继续从DEQUE获取工作 你几乎在回答你自己的问题。解决方案是声明“
ForkJoinPool
通过从join()
调用内部窃取其他线程的工作来避免此问题”。每当线程由于除ForkJoinPool.join()之外的其他原因被阻塞时,此工作窃取不会发生,线程只是等待,什么也不做
这是因为在Java中,ForkJoinPool
不可能阻止它的线程阻塞,而是给它们一些其他的工作。线程本身需要避免阻塞,而是向池请求它应该做的工作。这只在ForkJoinTask.join()方法中实现,而不在任何其他阻塞方法中实现。如果在ForkJoinPool
中使用Future
,还将看到饥饿死锁
为什么工作窃取只在ForkJoinTask.join()中实现,而不在Java API中的任何其他阻塞方法中实现?嗯,有很多这样的阻塞方法(Object.wait()
,Future.get()
,java.util.concurrent
中的任何并发原语,I/O方法等等),它们与ForkJoinPool
无关,后者只是API中的一个任意类,因此,在所有这些方法中添加特殊情况将是糟糕的设计。这也可能导致非常令人惊讶和不期望的影响。例如,假设一个用户将一个任务传递给一个等待未来的ExecutorService
,然后发现该任务在未来挂起很长时间。get()
仅仅是因为正在运行的线程窃取了其他线程(长时间运行)工作项,而不是等待未来
并在结果可用后立即继续。一旦一个线程开始处理另一个任务,它就不能返回到原始任务,直到第二个任务完成。因此,其他阻塞方法不起作用实际上是件好事。对于ForkJoinTask
,不存在此问题,因为尽快继续执行主要任务并不重要,重要的是尽可能有效地处理所有任务
在ForkJoinPool
中,也不可能实现您自己的工作窃取方法,因为所有相关部分都不是公共的
然而,实际上还有第二种方法可以防止饥饿死锁。这称为管理阻塞。它不使用工作窃取(以避免上述问题),但还需要将被阻塞的线程与线程池积极协作。对于托管阻塞,线程会在调用潜在阻塞方法之前通知线程池它可能已被阻塞,并在阻塞方法完成时通知线程池。然后,线程池知道存在饥饿死锁的风险,如果其所有线程当前都处于某个阻塞操作中,并且还有其他任务要执行,则可能会产生额外的线程。注意,由于额外线程的开销,这比工作窃取效率低。如果您使用普通未来和托管阻塞实现递归并行算法,而不是使用ForkJoinTask
和工作窃取,那么额外线程的数量可能会非常大(因为在“divide”中在算法阶段,将创建大量任务并将其分配给线程,这些线程立即阻塞并等待子任务的结果)。但是,饥饿死锁仍然是可以避免的,它避免了一个任务必须等待很长时间的问题,因为它的线程同时开始处理另一个任务
Java的ForkJoinPool
也支持托管阻塞。要使用它,需要实现接口,以便从该接口的block
方法中调用任务想要执行的潜在阻塞方法。然后任务可能不会直接调用阻塞方法,而是需要调用静态方法。此方法处理阻塞前后与线程池的通信。如果当前任务没有在ForkJoinPool
中执行,那么它也可以工作,然后只调用阻塞方法
我在JavaAPI(用于Java7)中发现的唯一实际使用托管阻塞的地方是类。(此类与互斥锁和锁存器类似,但更灵活、更强大。)因此,与ForkJoinPool
任务中的Phaser
同步应使用托管阻塞,并可避免饥饿死锁(但ForkJoinTask.join()
仍然更可取,因为它使用工作窃取而不是托管阻塞)。无论您是直接使用ForkJoinPool
还是