代码之家  ›  专栏  ›  技术社区  ›  gstackoverflow

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

  •  18
  • gstackoverflow  · 技术社区  · 7 年前

    警告: 这不是重复的,请阅读主题 认真地 https://stackoverflow.com/users/3448419/apangin 引述:

    真正的问题是为什么代码有时在不应该的情况下工作。 即使没有lambdas,问题也会重现。这让我想到了那里 可能是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 回复  |  直到 6 年前
        1
  •  19
  •   apangin    7 年前

    TL;博士 这是一个热点错误 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 显然未初始化(因为其静态初始值设定项仍在运行)。这意味着 螺纹-2 必须启动中所述的类初始化过程 JVMS §5.5 . 按照这个程序,,

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

    螺纹-2 未被阻止,尽管类正在通过线程进行初始化 main .

    其他JVM呢

    我测试了OpenJ9和JET,他们都期望在上述测试中死锁。
    有趣的是,热点也存在 -Xcomp 模式,但不在 -Xint

    它是怎么发生的

    字节码,它调用JVM运行时来解析方法引用。作为该过程的一部分,JVM会在必要时初始化该类。成功解析后,解析的方法保存在常量池缓存项中。常量池缓存是一种特定于热点的结构,用于存储解析的常量池值。

    在上述测试中 不动产 调用 静态目标 首先由 主要的 线解释器运行时跳过类初始化,因为该类已由同一线程初始化。解析的方法保存在常量池缓存中。下次什么时候 螺纹-2 执行相同的命令 ,解释器看到字节码已经解析,并使用常量池缓存条目而不调用运行时,因此跳过类初始化。

    getstatic / putstatic 很久以前就修好了- JDK-4493560 不动产 JDK-8215634

    至于原来的例子,,

    它是否挂起取决于哪个线程首先解析静态调用。如果是 主要的 线程,程序在没有死锁的情况下完成。如果静态调用由以下其中一个解决: ForkJoinPool 线程,程序挂起。

    错误是 confirmed

    推荐文章