【问题标题】:Why using parallel streams in static initializer leads to not stable deadlock为什么在静态初始化程序中使用并行流会导致不稳定的死锁
【发布时间】:2018-12-11 12:56:39
【问题描述】:

注意:并非重复,请仔细阅读主题 https://stackoverflow.com/users/3448419/apangin报价:

真正的问题是为什么代码有时会在不应该运行的情况下运行。 即使没有 lambda,问题也会重现。这让我想到那里 可能是 JVM 错误。

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

让我们考虑以下源代码:

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

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

【问题讨论】:

  • 在重复的问题中,not 到底解释了什么?答案归结为:“编写这样的代码会导致死锁。所以不要编写这样的代码”。您认为还可以添加什么?
  • 多线程的本质是对时间敏感。如果它总是失败,或者永远不会失败,那么“多个线程”就不会难了。
  • 所有可能的组合...可能适用于您的小示例,但考虑到任何实际的、现实的问题,您会迅速掠夺成倍增长的组合。而你唯一可以可能预测的就是出现僵局的可能性?
  • 不是重复的。真正的问题是为什么代码有时在它不应该工作的时候工作。即使没有 lambda,问题也会重现。这让我觉得可能存在 JVM 错误。稍后我会检查它。
  • 是的,我现在确信这是一个显然与常量池解析有关的 JVM 错误。我还发现了一个关于静态字段访问的非常古老的类似错误JDK-4493560。似乎该错误已针对 getstatic/putstatic 字节码修复,但不适用于 invokestatic

标签: java concurrency deadlock jvm-hotspot static-initialization


【解决方案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 字节码时这样做

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

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 的结构,用于存储已解析的常量池值。

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

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

至于原来的例子,

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

更新

错误是confirmed。它在即将发布的版本中得到修复:JDK 8u201、JDK 11.0.2 和 JDK 12。

【讨论】:

    猜你喜欢
    • 2016-04-21
    • 1970-01-01
    • 2019-04-03
    • 1970-01-01
    • 1970-01-01
    • 2015-03-09
    • 2019-12-27
    相关资源
    最近更新 更多