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

gst*_*low 18 java concurrency deadlock jvm-hotspot static-initialization

注意:这是不是重复,请仔细阅读题目сarefully /sf/users/241389361/报价:

真正的问题是为什么代码有时会起作用.即使没有lambdas,问题也会重现.这让我觉得可能存在JVM错误.

/sf/answers/3759645221/的评论中,我试图找出原因,为什么代码的行为从一个开始到另一个不同,并且该讨论的参与者给了我一些建议来创建一个单独的主题.

不要考虑以下源代码:

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");
    }
}
Run Code Online (Sandbox Code Playgroud)

有时(几乎总是)它会导致死锁.

输出示例:

static initializer: main
map: main 2
map: ForkJoinPool.commonPool-worker-3 4
map: ForkJoinPool.commonPool-worker-3 3
map: ForkJoinPool.commonPool-worker-2 0
Run Code Online (Sandbox Code Playgroud)

但有时它会成功完成(非常罕见):

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
Run Code Online (Sandbox Code Playgroud)

要么

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
Run Code Online (Sandbox Code Playgroud)

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

apa*_*gin 11

TL; DR这是一个HotSpot错误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) {
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Called from main
Called from Thread-2
Initialization complete
Run Code Online (Sandbox Code Playgroud)

为什么这是一个错误

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

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

Thread-2通话staticTarget,主类StaticInit显然是未初始化(因为它的静态初始化仍在运行).这意味着Thread-2必须启动JVMS§5.5中描述的类初始化过程.根据这个程序,

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

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

其他JVM怎么样?

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

怎么回事

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

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

一个类似的错误getstatic/ putstatic很久以前修复了- JDK-4493560,但修复没有触及invokestatic.我已经提交了新的错误JDK-8215634来解决这个问题.

至于原始的例子,

是否挂起取决于哪个线程首先解析静态调用.如果它是main线程,程序完成没有死锁.如果静态调用由其中一个ForkJoinPool线程解析,则程序挂起.

更新

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