在嵌套的Java8并行流操作中使用信号量可能会死锁。这是虫子吗?

在嵌套的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”控制,通常等于处理器的数量 现在假设我们喜欢限制特定工件的并行执行次数-例如,因为该部分是内存密集型的,内存约束意味着并行执行的次数有限 限制

考虑以下情况:我们使用Java 8并行流来执行并行forEach循环,例如

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
处。请注意,代码包括

  • 获取信号量
  • 运行一些代码
  • 释放信号量
  • 死锁发生在2。如果那段代码正在使用另一个并行流。然后死锁在另一个流中发生。因此,似乎不允许同时使用嵌套并行流和阻塞操作(如信号量)

    请注意,据文档记载,并行流使用ForkJoinPool,并且ForkJoinPool和信号量属于同一个包-
    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;