在嵌套的Java8并行流操作中使用信号量可能会死锁。这是虫子吗?
考虑以下情况:我们使用Java 8并行流来执行并行forEach循环,例如在嵌套的Java8并行流操作中使用信号量可能会死锁。这是虫子吗?,java,concurrency,parallel-processing,java-8,java-stream,Java,Concurrency,Parallel Processing,Java 8,Java Stream,考虑以下情况:我们使用Java 8并行流来执行并行forEach循环,例如 IntStream.range(0,20).parallel().forEach(i -> { /* work done here */}) 并行线程的数量由系统属性“java.util.concurrent.ForkJoinPool.common.parallelism”控制,通常等于处理器的数量 现在假设我们喜欢限制特定工件的并行执行次数-例如,因为该部分是内存密集型的,内存约束意味着并行执行的次数有限 限制
IntStream.range(0,20).parallel().forEach(i -> { /* work done here */})
并行线程的数量由系统属性“java.util.concurrent.ForkJoinPool.common.parallelism”控制,通常等于处理器的数量
现在假设我们喜欢限制特定工件的并行执行次数-例如,因为该部分是内存密集型的,内存约束意味着并行执行的次数有限
限制并行执行的一种明显而优雅的方法是使用信号量(建议),例如,以下代码将并行执行的数量限制为5:
final Semaphore concurrentExecutions = new Semaphore(5);
IntStream.range(0,20).parallel().forEach(i -> {
concurrentExecutions.acquireUninterruptibly();
try {
/* WORK DONE HERE */
}
finally {
concurrentExecutions.release();
}
});
这个很好用
但是:在工作进程内使用任何其他并行流(在/*此处完成的工作*/
)可能会导致死锁
对我来说,这是一种意想不到的行为
说明:由于Java流使用ForkJoin池,内部forEach正在进行forking,而连接似乎一直在等待。然而,这种行为仍然出乎意料。请注意,如果将“java.util.concurrent.ForkJoinPool.common.parallelism”
设置为1,并行流甚至可以工作
还要注意的是,如果有内部平行的forEach,它可能不是透明的
问题:这种行为是否符合Java 8规范(在这种情况下,它意味着禁止在并行流工作程序中使用信号量),或者这是一个bug
为了方便起见:下面是一个完整的测试用例。两个布尔值的任何组合都有效,但“true,true”除外,这会导致死锁
澄清:为了澄清这一点,让我强调一个方面:死锁不会发生在信号量的acquire
处。请注意,代码包括
java.util.concurrent
(因此人们希望它们能够很好地互操作)
我在探查器(VisualVM)中运行了您的测试,我同意:线程正在等待信号量,并在F/J池中等待join() 这个框架在join()方面存在严重问题。四年来,我一直在写一篇关于这个框架的评论。基本连接问题 waitjoin()也有类似的问题。您可以自己阅读代码。当框架到达工作deque的底部时,它会发出wait()。归根结底,这个框架无法进行上下文切换 有一种方法可以让这个框架为暂停的线程创建补偿线程。您需要实现ForkJoinPool.ManagedBlocker接口。你怎么能做到这一点,我不知道。您正在使用流运行一个基本API。您没有实现Streams API并编写自己的代码
我坚持上面的评论:一旦将并行性转换为API,就放弃了控制该并行机制内部工作的能力。API没有缺陷(除了使用错误的框架进行并行操作)。问题是,信号量或任何其他控制API内并行性的方法都是危险的想法。在对ForkJoinPool和ForkJoinTask的源代码进行了一点调查之后,我想我找到了一个答案: 这是一个bug(在我看来),这个bug出现在
ForkJoinTask
的doInvoke()
中。问题实际上与两个循环的嵌套有关,可能与信号量的使用无关,但是,需要信号量(或外循环中的s.th.阻塞)才能使问题变得明显并导致死锁(但我可以想象此错误隐含了其他问题-请参阅)
doInvoke()
方法的实现如下所示:
/**
* Implementation for invoke, quietlyInvoke.
*
* @return status upon completion
*/
private int doInvoke() {
int s; Thread t; ForkJoinWorkerThread wt;
return (s = doExec()) < 0 ? s :
((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
(wt = (ForkJoinWorkerThread)t).pool.awaitJoin(wt.workQueue, this) :
externalAwaitDone();
}
测试Thread.currentThread()是否为ForkJoinWorkerThread的实例。此测试的原因是检查ForkJoinTask是在池的工作线程上运行还是在主线程上运行。我相信这一行对于非嵌套的并行for是合适的,它允许区分当前任务是在主线程上运行还是在池工作线程上运行。但是,对于内部循环的任务,这个测试是有问题的:让我们调用运行parallel()的线程。为每个creator线程调用。对于外部循环,创建者线程是主线程,而不是ForkJoinWorkerThread的实例。但是,对于从ForkJoinWorkerThread
运行的内部循环,创建者线程也是ForkJoinWorkerThread
的实例。因此,在这种情况下,ForkJoinWorkerThread的测试((t=Thread.currentThread())实例总是正确的
因此,我们总是调用pool.awaitJoint(wt.workQueue)
现在,请注意,我们在该线程的完整工作队列上调用awaitJoint
(我认为这是一个额外的缺陷)。看起来,我们不仅加入了内部循环任务,还加入了外部循环的任务,并且加入了所有这些任务。不幸的是,外部任务包含该信号量
为了证明bug与此相关,我们可以检查一个非常简单的解决方法。我创建一个运行内部循环的t=new Thread()
,然后执行t.start();t、 join()代码>。请注意,这不会引入任何额外的并行性(我将立即加入)。但是,
/**
* Implementation for invoke, quietlyInvoke.
*
* @return status upon completion
*/
private int doInvoke() {
int s; Thread t; ForkJoinWorkerThread wt;
return (s = doExec()) < 0 ? s :
((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
(wt = (ForkJoinWorkerThread)t).pool.awaitJoin(wt.workQueue, this) :
externalAwaitDone();
}
((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
final boolean isUseSemaphore = true;
final boolean isUseInnerStream = true;
final boolean isWrappedInnerLoopThread = false;
final boolean isUseSemaphore = true;
final boolean isUseInnerStream = true;
final boolean isWrappedInnerLoopThread = true;