为什么在静态初始化程序中使用 lambda 的并行流会导致死锁?
Why does parallel stream with lambda in static initializer cause a deadlock?
我遇到了一个奇怪的情况,在静态初始化程序中使用带有 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) {}
}
这似乎是此行为的最小重现测试用例。如果我:
- 将块放在 main 方法中而不是静态初始化程序中,
- 删除并行化,或
- 删除 lambda,
代码立即完成。谁能解释这种行为?这是错误还是有意为之?
我正在使用 OpenJDK 1.8 版。0_66-internal。
我发现了一个非常相似的案例 (JDK-8143380) 的错误报告,该案例已被 Stuart Marks 关闭为 "Not an Issue":
This is a class initialization deadlock. The test program's main thread executes the class static initializer, which sets the initialization in-progress flag for the class; this flag remains set until the static initializer completes. The static initializer executes a parallel stream, which causes lambda expressions to be evaluated in other threads. Those threads block waiting for the class to complete initialization. However, the main thread is blocked waiting for the parallel tasks to complete, resulting in deadlock.
The test program should be changed to move the parallel stream logic outside of the class static initializer. Closing as Not an Issue.
我找到了另一个错误报告 (JDK-8136753),也被 Stuart Marks 关闭为 "Not an Issue":
This is a deadlock that is occurring because the Fruit enum's static initializer is interacting badly with class initialization.
See the Java Language Specification, section 12.4.2 for details on class initialization.
http://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.4.2
Briefly, what's happening is as follows.
- The main thread references the Fruit class and starts the initialization process. This sets the initialization in-progress flag and runs the static initializer on the main thread.
- The static initializer runs some code in another thread and waits for it to finish. This example uses parallel streams, but this has nothing to do with streams per se. Executing code in another thread by any means, and waiting for that code to finish, will have the same effect.
- The code in the other thread references the Fruit class, which checks the initialization in-progress flag. This causes the other thread to block until the flag is cleared. (See step 2 of JLS 12.4.2.)
- The main thread is blocked waiting for the other thread to terminate, so the static initializer never completes. Since the initialization in-progress flag isn't cleared until after the static initializer completes, the threads are deadlocked.
To avoid this problem, make sure that a class's static initialization completes quickly, without causing other threads to execute code that requires this class to have completed initialization.
Closing as Not an Issue.
对于那些想知道引用 Deadlock
class 本身的其他线程在哪里的人,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) {}
}
使用常规匿名 classes 没有死锁:
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) {}
}
Andrei Pangin, dated by 07 Apr 2015. It is available here 对这个问题有很好的解释,但它是用俄语写的(我建议无论如何都要查看代码示例 - 它们是国际性的)。一般问题是在 class 初始化期间锁定。
以下是文章中的一些引述:
根据 JLS,每个 class 都有一个唯一的 初始化锁 在初始化期间捕获。当其他线程在初始化期间尝试访问此 class 时,它将被锁定,直到初始化完成。当class并发初始化时,有可能出现死锁。
我写了一个计算整数和的简单程序,它应该打印什么?
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 - 会有什么变化?
这里我们又看到了死锁[文章前面的 class 初始化程序中有一些死锁的例子]。因为 parallel()
流操作 运行 在单独的线程池中。这些线程尝试执行 lambda 主体,它在字节码中作为 StreamSum
class 中的 private static
方法编写。但是这个方法不能在 class 静态初始化器完成之前执行,它等待流完成的结果。
更令人兴奋的是:此代码在不同环境中的工作方式不同。它将在单台 CPU 机器上正常工作,并且很可能在多台 CPU 机器上挂起。这种差异来自 Fork-Join 池实现。您可以自己更改参数 -Djava.util.concurrent.ForkJoinPool.common.parallelism=N
进行验证
我遇到了一个奇怪的情况,在静态初始化程序中使用带有 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) {}
}
这似乎是此行为的最小重现测试用例。如果我:
- 将块放在 main 方法中而不是静态初始化程序中,
- 删除并行化,或
- 删除 lambda,
代码立即完成。谁能解释这种行为?这是错误还是有意为之?
我正在使用 OpenJDK 1.8 版。0_66-internal。
我发现了一个非常相似的案例 (JDK-8143380) 的错误报告,该案例已被 Stuart Marks 关闭为 "Not an Issue":
This is a class initialization deadlock. The test program's main thread executes the class static initializer, which sets the initialization in-progress flag for the class; this flag remains set until the static initializer completes. The static initializer executes a parallel stream, which causes lambda expressions to be evaluated in other threads. Those threads block waiting for the class to complete initialization. However, the main thread is blocked waiting for the parallel tasks to complete, resulting in deadlock.
The test program should be changed to move the parallel stream logic outside of the class static initializer. Closing as Not an Issue.
我找到了另一个错误报告 (JDK-8136753),也被 Stuart Marks 关闭为 "Not an Issue":
This is a deadlock that is occurring because the Fruit enum's static initializer is interacting badly with class initialization.
See the Java Language Specification, section 12.4.2 for details on class initialization.
http://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.4.2
Briefly, what's happening is as follows.
- The main thread references the Fruit class and starts the initialization process. This sets the initialization in-progress flag and runs the static initializer on the main thread.
- The static initializer runs some code in another thread and waits for it to finish. This example uses parallel streams, but this has nothing to do with streams per se. Executing code in another thread by any means, and waiting for that code to finish, will have the same effect.
- The code in the other thread references the Fruit class, which checks the initialization in-progress flag. This causes the other thread to block until the flag is cleared. (See step 2 of JLS 12.4.2.)
- The main thread is blocked waiting for the other thread to terminate, so the static initializer never completes. Since the initialization in-progress flag isn't cleared until after the static initializer completes, the threads are deadlocked.
To avoid this problem, make sure that a class's static initialization completes quickly, without causing other threads to execute code that requires this class to have completed initialization.
Closing as Not an Issue.
对于那些想知道引用 Deadlock
class 本身的其他线程在哪里的人,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) {}
}
使用常规匿名 classes 没有死锁:
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) {}
}
Andrei Pangin, dated by 07 Apr 2015. It is available here 对这个问题有很好的解释,但它是用俄语写的(我建议无论如何都要查看代码示例 - 它们是国际性的)。一般问题是在 class 初始化期间锁定。
以下是文章中的一些引述:
根据 JLS,每个 class 都有一个唯一的 初始化锁 在初始化期间捕获。当其他线程在初始化期间尝试访问此 class 时,它将被锁定,直到初始化完成。当class并发初始化时,有可能出现死锁。
我写了一个计算整数和的简单程序,它应该打印什么?
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 - 会有什么变化?
这里我们又看到了死锁[文章前面的 class 初始化程序中有一些死锁的例子]。因为 parallel()
流操作 运行 在单独的线程池中。这些线程尝试执行 lambda 主体,它在字节码中作为 StreamSum
class 中的 private static
方法编写。但是这个方法不能在 class 静态初始化器完成之前执行,它等待流完成的结果。
更令人兴奋的是:此代码在不同环境中的工作方式不同。它将在单台 CPU 机器上正常工作,并且很可能在多台 CPU 机器上挂起。这种差异来自 Fork-Join 池实现。您可以自己更改参数 -Djava.util.concurrent.ForkJoinPool.common.parallelism=N