我遇到了一个奇怪的情况,即在静态初始化程序中使用带有lambda的并行流看似永远没有CPU利用率。这是代码:
class Deadlock {
static {
IntStream.range(0, 10000).parallel().map(i -> i).count();
System.out.println("done");
}
public static void main(final String[] args) {}
}
这似乎是此行为的最小重现测试用例。如果我:
代码立即完成。谁能解释这种行为?这是一个错误还是这个?
我使用的是OpenJDK版本1.8.0_66-internal。
答案 0 :(得分:66)
我发现了一个非常相似的案例(JDK-8143380)的错误报告,该案例被关闭为"不是问题"作者:Stuart Marks:
这是一个类初始化死锁。测试程序的主线程执行类静态初始化器,它为类设置初始化进行中标志;静态初始化程序完成之前,此标志保持设置状态。静态初始化程序执行并行流,这会导致在其他线程中计算lambda表达式。这些线程阻止等待类完成初始化。但是,主线程被阻塞等待并行任务完成,从而导致死锁。
应该更改测试程序以将并行流逻辑移到类静态初始化程序之外。关闭不是问题。
我能够找到另一个错误报告(JDK-8136753),也被关闭为"不是问题"作者:Stuart Marks:
这是一个死锁,因为Fruit enum的静态初始化程序与类初始化交互不良。
有关类初始化的详细信息,请参阅Java语言规范,第12.4.2节。
http://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.4.2
简而言之,发生的事情如下:
- 主线程引用Fruit类并启动初始化过程。这将设置初始化进行中标志并在主线程上运行静态初始化程序。
- 静态初始化程序在另一个线程中运行一些代码并等待它完成。此示例使用并行流,但这与流本身无关。以任何方式在另一个线程中执行代码,并等待该代码完成,将产生相同的效果。
- 另一个线程中的代码引用Fruit类,它检查初始化进行中的标志。这会导致另一个线程阻塞,直到清除该标志。 (参见JLS 12.4.2的第2步。)
- 主线程被阻塞,等待另一个线程终止,因此静态初始化器永远不会完成。由于初始化进行中标志未被清除,直到静态初始化程序完成后,线程才会死锁。
醇>要避免此问题,请确保类的静态初始化快速完成,而不会导致其他线程执行要求此类完成初始化的代码。
关闭不是问题。
答案 1 :(得分:16)
对于那些想知道其他线程在哪里引用Deadlock
类本身的人来说,Java lambdas的行为就像你写的那样:
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) {}
}
答案 2 :(得分:13)
2015年4月7日Andrei Pangin对此问题有一个很好的解释。它可用here,但它是用俄语写的(我建议无论如何审查代码示例 - 它们是国际)。一般问题是在类初始化期间锁定。
以下是文章中的一些引用:
根据JLS,每个类都有一个在初始化期间捕获的唯一初始化锁。当其他线程在初始化期间尝试访问此类时,它将在锁定时被阻止,直到初始化完成。当同时初始化类时,可能会出现死锁。
我写了一个简单的程序来计算整数的总和,它应该打印什么?
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()
或用Integer::sum
调用替换lambda - 会有什么变化?
这里我们再次看到死锁[在本文前面的类初始化器中有一些死锁的例子]。由于parallel()
流操作在单独的线程池中运行。这些线程尝试执行lambda body,它在字节码中用private static
类内的StreamSum
方法编写。但是这个方法不能在类静态初始化器完成之前执行,它会等待流完成的结果。
更有意思的是:此代码在不同环境中的工作方式不同。它可以在单个CPU机器上正常工作,并且很可能挂在多CPU机器上。这种差异来自Fork-Join池实现。您可以自行更改参数-Djava.util.concurrent.ForkJoinPool.common.parallelism=N