为什么在静态初始值设定项中使用并行流会导致不稳定的死锁

注意:这不是重复的,请阅读主题сare https://stackoverflow.com/users/3448419/apangin 引用:

真正的问题是为什么代码有时在不应该工作的时候工作。即使没有 lambda,问题也会重现。这让我觉得可能有一个JVM错误。

https://stackoverflow.com/a/53709217/2674303 的评论中,我试图找出为什么代码从一个开始到另一个开始的行为不同,讨论的参与者向我提出了一条建议,以创建一个单独的主题。

让我们考虑以下源代码:

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

你能解释一下这种行为吗?


答案 1

TL;DR这是一个热点错误 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) {
    }
}

这看起来像一个经典的初始化死锁,但HotSpot JVM不会挂起。相反,它打印:

Called from main
Called from Thread-2
Initialization complete

为什么这是一个错误

JVMS §6.5 要求在执行字节码时invokestatic

如果声明已解析方法的类或接口尚未初始化,则初始化该类或接口

当调用时,主类显然是未初始化的(因为它的静态初始值设定项仍在运行)。这意味着必须启动 JVMS §5.5 中描述的类初始化过程。根据此程序,Thread-2staticTargetStaticInitThread-2

  1. 如果 C 的 Class 对象指示某个其他线程正在对 C 进行初始化,则释放 LC 并阻止当前线程,直到通知正在进行的初始化已完成

但是,尽管该类正在通过线程进行初始化,但 仍未被阻止。Thread-2main

其他 JVM 呢?

我测试了OpenJ9和JET,预计它们都会在上述测试中陷入僵局。
有趣的是,HotSpot也以模式挂起,但不在混合模式下挂起。-Xcomp-Xint

它是如何发生的

当解释器第一次遇到字节码时,它会调用 JVM 运行时来解析方法引用。作为此过程的一部分,JVM 会在必要时初始化类。成功解决后,解析的方法将保存在常量池缓存条目中。常量池缓存是特定于 HotSpot 的结构,用于存储已解析的常量池值。invokestatic

在上面的测试字节码中,调用首先由线程解析。解释器运行时跳过类初始化,因为类已由同一线程初始化。解析的方法保存在常量池缓存中。下次执行相同的操作时,解释器会看到字节码已经解析,并使用常量池缓存条目而不调用运行时,因此跳过类初始化。invokestaticstaticTargetmainThread-2invokestatic

很久以前就修复了 / 的类似错误 - JDK-4493560,但修复程序没有触及 。我已提交新的 bug JDK-8215634 来解决此问题。getstaticputstaticinvokestatic

至于原来的例子,

它是否挂起取决于哪个线程首先解析静态调用。如果是线程,程序将完成而不会出现死锁。如果静态调用由其中一个线程解析,则程序将挂起。mainForkJoinPool

更新

确认错误。它在即将发布的版本中得到了修复:JDK 8u201,JDK 11.0.2和JDK 12。


答案 2

推荐