为什么在静态初始化程序中使用并行流会导致不稳定的死锁 [英] Why using parallel streams in static initializer leads to not stable deadlock

查看:125
本文介绍了为什么在静态初始化程序中使用并行流会导致不稳定的死锁的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

注意:它不是重复的,请仔细阅读主题 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

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

解决方案

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字节码

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

Thread-2调用staticTarget时,主类StaticInit显然未初始化(因为其静态初始化程序仍在运行).这意味着Thread-2必须启动 JVMS§5.5.按照此步骤,

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

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

其他JVM怎么样

我测试了OpenJ9和JET,预期它们在上述测试中都死锁了.
有趣的是,HotSpot也可以挂在-Xcomp模式下,但不能挂在-Xint或混合模式下.

它如何发生

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

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

很早以前就解决了getstatic/putstatic的类似错误- JDK- 4493560 ,但此修复程序未触及invokestatic.我已经提交了新的错误 JDK-8215634 来解决此问题. >

对于原始示例,

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

更新

该错误是https://stackoverflow.com/users/3448419/apangin quote:

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.

In the comments of https://stackoverflow.com/a/53709217/2674303 I tried to find out reasons why code behaves differently from one start to another and participants of that discussion made me piece of of advice to create a separated topic.

Let's consider following source code:

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");
    }
}

Sometimes(almost always) it leads to deadlock.

Example of output:

static initializer: main
map: main 2
map: ForkJoinPool.commonPool-worker-3 4
map: ForkJoinPool.commonPool-worker-3 3
map: ForkJoinPool.commonPool-worker-2 0

But sometimes it finishes successfully(very rare):

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

or

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

Could you explain that behaviour?

解决方案

TL;DR This is a HotSpot bug JDK-8215634

The problem can be reproduced with a simple test case that has no races at all:

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) {
    }
}

This looks like a classic initialization deadlock, but HotSpot JVM does not hang. Instead it prints:

Called from main
Called from Thread-2
Initialization complete

Why this is a bug

JVMS §6.5 requires that upon execution of invokestatic bytecode

the class or interface that declared the resolved method is initialized if that class or interface has not already been initialized

When Thread-2 calls staticTarget, the main class StaticInit is obviously uninitialized (since its static initializer is still running). This means Thread-2 must launch class initialization procedure described in JVMS §5.5. According to this procedure,

  1. 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

However, Thread-2 is not blocked despite the class is in progress of initialization by thread main.

What about other JVMs

I tested OpenJ9 and JET, and they both expectedly deadlock on the above test.
It's interesting that HotSpot also hangs in -Xcomp mode, but not in -Xint or mixed modes.

How it happens

When interpreter first encounters invokestatic bytecode, it calls JVM runtime to resolve the method reference. As a part of this process JVM initializes the class if necessary. After successful resolution the resolved method is saved in the Constant Pool Cache entry. Constant Pool Cache is a HotSpot-specific structure that stores resolved constant pool values.

In the above test invokestatic bytecode that calls staticTarget is first resolved by the main thread. Interpreter runtime skips class initialization, because the class is already being initialized by the same thread. The resolved method is saved in the constant pool cache. The next time when Thread-2 executes the same invokestatic, the interpreter sees that the bytecode is already resolved and uses constant pool cache entry without calling to runtime and thus skips class initialization.

A similar bug for getstatic/putstatic was fixed long ago - JDK-4493560, but the fix did not touch invokestatic. I've submitted the new bug JDK-8215634 to address this issue.

As to the original example,

whether it hangs or not depends on which thread first resolves the static call. If it is main thread, the program completes without a deadlock. If the static call is resolved by one of ForkJoinPool threads, the program hangs.

Update

The bug is confirmed. It is fixed in the upcoming releases: JDK 8u201, JDK 11.0.2 and JDK 12.

这篇关于为什么在静态初始化程序中使用并行流会导致不稳定的死锁的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆