Rei*_*ica 82 java deadlock fork-join java-8 java-stream
我遇到了一个奇怪的情况,在静态初始化程序中使用带有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) {}
}
Run Code Online (Sandbox Code Playgroud)
这似乎是此行为的最小再现测试用例.如果我:
代码立即完成.谁能解释这种行为?这是一个错误还是这个意图?
我正在使用OpenJDK版本1.8.0_66-internal.
Tun*_*aki 70
我发现了一个非常相似的情况下(的bug报告JDK-8143380这是封闭的"不是一个问题"司徒雷登商标):
这是一个类初始化死锁.测试程序的主线程执行类静态初始化程序,它为类设置初始化进程中的标志; 静态初始化程序完成之前,此标志保持设置状态.静态初始化程序执行并行流,这会导致在其他线程中计算lambda表达式.这些线程阻止等待类完成初始化.但是,主线程被阻塞等待并行任务完成,从而导致死锁.
应更改测试程序以将并行流逻辑移出类静态初始化程序之外.关闭不是问题.
我能找到(另一个bug报告JDK-8136753),也关闭了为"不是一个问题"斯图尔特标志:
这是一个死锁,因为Fruit枚举的静态初始化程序与类初始化交互不良.
有关类初始化的详细信息,请参阅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步.)
- 主线程被阻塞,等待另一个线程终止,因此静态初始化器永远不会完成.由于在静态初始化程序完成之前不会清除初始化进行中标志,因此线程会死锁.
要避免此问题,请确保类的静态初始化快速完成,而不会导致其他线程执行要求此类完成初始化的代码.
关闭不是问题.
请注意,FindBugs有一个未解决的问题,即为此情况添加警告.
Tam*_*dus 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) {}
}
Run Code Online (Sandbox Code Playgroud)
使用常规匿名类没有死锁:
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) {}
}
Run Code Online (Sandbox Code Playgroud)
Ada*_*ker 13
Andrei Pangin在2015年4月7日对此问题进行了很好的解释.它可以在这里找到,但它是用俄语写的(我建议无论如何都要审查代码示例 - 它们是国际的).一般问题是在类初始化期间锁定.
以下是文章中的一些引用:
根据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);
}
}
Run Code Online (Sandbox Code Playgroud)
现在parallel()用Integer::sumcall调用删除或替换lambda - 会有什么变化?
在这里,我们再次看到死锁[在本文前面的类初始化器中有一些死锁的例子].由于parallel()流操作在单独的线程池中运行.这些线程尝试执行lambda body,它以字节码编写,作为类中的private static方法StreamSum.但是这个方法不能在类静态初始化器完成之前执行,它等待流完成的结果.
更有意思的是:此代码在不同环境中的工作方式不同.它可以在单个CPU机器上正常工作,并且很可能挂在多CPU机器上.这种差异来自Fork-Join池实现.您可以自己更改参数进行验证-Djava.util.concurrent.ForkJoinPool.common.parallelism=N
| 归档时间: |
|
| 查看次数: |
4222 次 |
| 最近记录: |