Java 为什么在静态初始值设定项中使用lambda的并行流会导致死锁?
我遇到了一个奇怪的情况,在静态初始值设定项中使用带有lambda的并行流似乎要花费很长时间,而且没有CPU利用率。代码如下:Java 为什么在静态初始值设定项中使用lambda的并行流会导致死锁?,java,java-8,deadlock,java-stream,fork-join,Java,Java 8,Deadlock,Java Stream,Fork Join,我遇到了一个奇怪的情况,在静态初始值设定项中使用带有lambda的并行流似乎要花费很长时间,而且没有CPU利用率。代码如下: class Deadlock { static { IntStream.range(0, 10000).parallel().map(i -> i).count(); System.out.println("done"); } public static void main(final String[] arg
class Deadlock {
static {
IntStream.range(0, 10000).parallel().map(i -> i).count();
System.out.println("done");
}
public static void main(final String[] args) {}
}
这似乎是此行为的最小复制测试用例。如果我:
- 将块放在main方法中,而不是静态初始值设定项中
- 删除并行化,或
- 拆下lambda
我正在使用OpenJDK版本1.8.0_66-internal。我发现了一个非常类似的案例()的错误报告,Stuart Marks以“非问题”结束了该案例: 这是一个类初始化死锁。测试程序的主线程执行类静态初始化器,该初始化器设置类的初始化进行中标志;此标志保持设置状态,直到静态初始值设定项完成。静态初始值设定项执行并行流,这将导致在其他线程中计算lambda表达式。这些线程阻塞等待类完成初始化。但是,主线程在等待并行任务完成时被阻塞,导致死锁 应更改测试程序,以将并行流逻辑移到类静态初始值设定项之外。关闭不是一个问题
我找到了另一个关于该()的bug报告,Stuart Marks也以“非问题”结尾: 这是一个死锁,因为Fruit枚举的静态初始值设定项与类初始化交互不好 有关类初始化的详细信息,请参阅Java语言规范第12.4.2节 简而言之,发生的情况如下
请注意,对于这种情况。对于那些想知道引用
Deadlock
类本身的其他线程在哪里的人,Java lambda的行为与您编写的类似:
public class Deadlock {
public static int lambda1(int i) {
return i;
}
static {
IntStream.range(0, 10000).parallel().map(new IntUnaryOperator() {
@Override
public int applyAsInt(int operand) {
return lambda1(operand);
}
}).count();
System.out.println("done");
}
public static void main(final String[] args) {}
}
对于常规匿名类,没有死锁:
public class Deadlock {
static {
IntStream.range(0, 10000).parallel().map(new IntUnaryOperator() {
@Override
public int applyAsInt(int operand) {
return operand;
}
}).count();
System.out.println("done");
}
public static void main(final String[] args) {}
}
2015年4月7日,对该问题有一个很好的解释。它是可用的,但它是用俄语编写的(我建议无论如何都要检查代码示例——它们是国际性的)。一般的问题是类初始化期间的锁 以下是文章中的一些引文:
根据,每个类都有一个在初始化期间捕获的唯一初始化锁。当其他线程在初始化期间尝试访问此类时,它将被锁定,直到初始化完成。当类同时初始化时,可能会出现死锁 我写了一个简单的程序来计算整数的和,它应该打印什么
public class StreamSum {
static final int SUM = IntStream.range(0, 100).parallel().reduce((n, m) -> n + m).getAsInt();
public static void main(String[] args) {
System.out.println(SUM);
}
}
现在删除parallel()
或将lambda替换为Integer::sum
调用-会有什么变化
这里我们再次看到死锁[在本文前面的类初始值设定项中有一些死锁示例]。由于parallel()
流操作在单独的线程池中运行。这些线程尝试执行lambda body,它作为StreamSum
类中的private static
方法以字节码编写。但是在类静态初始值设定项完成之前不能执行此方法,该初始化项将等待流完成的结果
更令人兴奋的是:这段代码在不同的环境中工作方式不同。它将在单CPU机器上正常工作,并且很可能挂起在多CPU机器上。这种差异来自Fork-Join池实现。您可以自己更改参数
-Djava.util.concurrent.ForkJoinPool.common.parallelism=N
,范围为(0,1),程序正常终止。具有(0,2)或更高的挂起。类似问题:实际上是完全相同的问题/问题,只是具有不同的API。您正在尝试在后台线程中使用类,当您尚未完成类的初始化,因此无法在后台线程中使用时。@Solomonoff的cret asi->i
不是方法引用,而是在死锁类中实现的静态方法。如果用Function.identity()
替换i->i
,则此代码应该可以。@Solomonoff's确认这是一个实现选择。lambda中的代码必须放在某个地方。Javac将其编译成包含类中的静态方法(类似于本例中的lambda1
i)。将每个lambda放在自己的类中会花费相当大的成本。@StuartMarks如果lambda创建了一个实现函数接口的类,那么将lambda的实现放在自己的类中不是同样有效吗