为什么在静态初始化器中使用并行流会导致不稳定的死锁
Why using parallel streams in static initializer leads to not stable deadlock
注意:它不是重复的,请仔细阅读主题сarefully
https://whosebug.com/users/3448419/apangin 引述:
The real question is why the code sometimes works when it should not.
The issue reproduces even without lambdas. This makes me think there
might be a JVM bug.
在 的评论中,我试图找出代码从一开始到另一次表现不同的原因,那次讨论的参与者向我提出了创建一个单独主题的建议。
让我们考虑以下源代码:
public class Test {
static {
System.out.println("static initializer: " + Thread.currentThread().getName());
final long SUM = IntStream.range(0, 5)
.parallel()
.mapToObj(i -> {
System.out.println("map: " + Thread.currentThread().getName() + " " + i);
return i;
})
.sum();
}
public static void main(String[] args) {
System.out.println("Finished");
}
}
有时(几乎总是)它会导致死锁。
输出示例:
static initializer: main
map: main 2
map: ForkJoinPool.commonPool-worker-3 4
map: ForkJoinPool.commonPool-worker-3 3
map: ForkJoinPool.commonPool-worker-2 0
但有时它会成功完成(非常罕见):
static initializer: main
map: main 2
map: main 3
map: ForkJoinPool.commonPool-worker-2 4
map: ForkJoinPool.commonPool-worker-1 1
map: ForkJoinPool.commonPool-worker-3 0
Finished
或
static initializer: main
map: main 2
map: ForkJoinPool.commonPool-worker-2 0
map: ForkJoinPool.commonPool-worker-1 1
map: ForkJoinPool.commonPool-worker-3 4
map: main 3
你能解释一下这种行为吗?
TL;DR This is a HotSpot bug JDK-8215634
问题可以通过一个完全没有竞争的简单测试用例重现:
public class StaticInit {
static void staticTarget() {
System.out.println("Called from " + Thread.currentThread().getName());
}
static {
Runnable r = new Runnable() {
public void run() {
staticTarget();
}
};
r.run();
Thread thread2 = new Thread(r, "Thread-2");
thread2.start();
try { thread2.join(); } catch (Exception ignore) {}
System.out.println("Initialization complete");
}
public static void main(String[] args) {
}
}
这看起来像是 classic 初始化死锁,但 HotSpot JVM 不会挂起。相反,它打印:
Called from main
Called from Thread-2
Initialization complete
为什么这是一个错误
JVMS §6.5 要求在执行 invokestatic
字节码时
the class or interface that declared the resolved method is initialized if that class or interface has not already been initialized
当Thread-2
调用staticTarget
时,主classStaticInit
显然是未初始化的(因为它的静态初始化器仍然是运行)。这意味着 Thread-2
必须启动 class 在 JVMS §5.5 中描述的初始化程序。按照这个程序,
- If the Class object for C indicates that initialization is in progress for C by some other thread, then release LC and block the current thread until informed that the in-progress initialization has completed
但是,尽管 class 正在由线程 main
进行初始化,但 Thread-2
并未被阻塞。
其他 JVM 呢
我测试了 OpenJ9 和 JET,它们都在上述测试中出现了死锁。
有趣的是,HotSpot 在 -Xcomp
模式下也会挂起,但在 -Xint
或混合模式下不会。
它是如何发生的
当解释器第一次遇到 invokestatic
字节码时,它调用 JVM 运行时来解析方法引用。作为此过程的一部分,JVM 在必要时初始化 class。解析成功后解析的方法保存在常量池缓存条目中。常量池缓存是一个特定于 HotSpot 的结构,用于存储已解析的常量池值。
在上面的测试中,调用staticTarget
的invokestatic
字节码首先被main
线程解析。解释器运行时跳过 class 初始化,因为 class 已经被同一个线程初始化。解析后的方法保存在常量池缓存中。下次 Thread-2
执行相同的 invokestatic
时,解释器看到字节码已经解析并使用常量池缓存条目而不调用运行时,因此跳过 class 初始化。
很久以前就修复了 getstatic
/putstatic
的类似错误 - JDK-4493560, but the fix did not touch invokestatic
. I've submitted the new bug JDK-8215634 以解决此问题。
关于原来的例子,
挂不挂取决于哪个线程先解析静态调用。如果它是 main
线程,程序将在没有死锁的情况下完成。如果静态调用由 ForkJoinPool
个线程之一解析,程序将挂起。
更新
错误是 confirmed。它已在即将发布的版本中修复:JDK 8u201、JDK 11.0.2 和 JDK 12.
注意:它不是重复的,请仔细阅读主题сarefully https://whosebug.com/users/3448419/apangin 引述:
The real question is why the code sometimes works when it should not. The issue reproduces even without lambdas. This makes me think there might be a JVM bug.
在
让我们考虑以下源代码:
public class Test {
static {
System.out.println("static initializer: " + Thread.currentThread().getName());
final long SUM = IntStream.range(0, 5)
.parallel()
.mapToObj(i -> {
System.out.println("map: " + Thread.currentThread().getName() + " " + i);
return i;
})
.sum();
}
public static void main(String[] args) {
System.out.println("Finished");
}
}
有时(几乎总是)它会导致死锁。
输出示例:
static initializer: main
map: main 2
map: ForkJoinPool.commonPool-worker-3 4
map: ForkJoinPool.commonPool-worker-3 3
map: ForkJoinPool.commonPool-worker-2 0
但有时它会成功完成(非常罕见):
static initializer: main
map: main 2
map: main 3
map: ForkJoinPool.commonPool-worker-2 4
map: ForkJoinPool.commonPool-worker-1 1
map: ForkJoinPool.commonPool-worker-3 0
Finished
或
static initializer: main
map: main 2
map: ForkJoinPool.commonPool-worker-2 0
map: ForkJoinPool.commonPool-worker-1 1
map: ForkJoinPool.commonPool-worker-3 4
map: main 3
你能解释一下这种行为吗?
TL;DR This is a HotSpot bug JDK-8215634
问题可以通过一个完全没有竞争的简单测试用例重现:
public class StaticInit {
static void staticTarget() {
System.out.println("Called from " + Thread.currentThread().getName());
}
static {
Runnable r = new Runnable() {
public void run() {
staticTarget();
}
};
r.run();
Thread thread2 = new Thread(r, "Thread-2");
thread2.start();
try { thread2.join(); } catch (Exception ignore) {}
System.out.println("Initialization complete");
}
public static void main(String[] args) {
}
}
这看起来像是 classic 初始化死锁,但 HotSpot JVM 不会挂起。相反,它打印:
Called from main
Called from Thread-2
Initialization complete
为什么这是一个错误
JVMS §6.5 要求在执行 invokestatic
字节码时
the class or interface that declared the resolved method is initialized if that class or interface has not already been initialized
当Thread-2
调用staticTarget
时,主classStaticInit
显然是未初始化的(因为它的静态初始化器仍然是运行)。这意味着 Thread-2
必须启动 class 在 JVMS §5.5 中描述的初始化程序。按照这个程序,
- If the Class object for C indicates that initialization is in progress for C by some other thread, then release LC and block the current thread until informed that the in-progress initialization has completed
但是,尽管 class 正在由线程 main
进行初始化,但 Thread-2
并未被阻塞。
其他 JVM 呢
我测试了 OpenJ9 和 JET,它们都在上述测试中出现了死锁。
有趣的是,HotSpot 在 -Xcomp
模式下也会挂起,但在 -Xint
或混合模式下不会。
它是如何发生的
当解释器第一次遇到 invokestatic
字节码时,它调用 JVM 运行时来解析方法引用。作为此过程的一部分,JVM 在必要时初始化 class。解析成功后解析的方法保存在常量池缓存条目中。常量池缓存是一个特定于 HotSpot 的结构,用于存储已解析的常量池值。
在上面的测试中,调用staticTarget
的invokestatic
字节码首先被main
线程解析。解释器运行时跳过 class 初始化,因为 class 已经被同一个线程初始化。解析后的方法保存在常量池缓存中。下次 Thread-2
执行相同的 invokestatic
时,解释器看到字节码已经解析并使用常量池缓存条目而不调用运行时,因此跳过 class 初始化。
很久以前就修复了 getstatic
/putstatic
的类似错误 - JDK-4493560, but the fix did not touch invokestatic
. I've submitted the new bug JDK-8215634 以解决此问题。
关于原来的例子,
挂不挂取决于哪个线程先解析静态调用。如果它是 main
线程,程序将在没有死锁的情况下完成。如果静态调用由 ForkJoinPool
个线程之一解析,程序将挂起。
更新
错误是 confirmed。它已在即将发布的版本中修复:JDK 8u201、JDK 11.0.2 和 JDK 12.