当前位置: 首页 > 面试题库 >

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

韩英锐
2023-03-14
问题内容

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

真正的问题是为什么代码有时在不应该运行的情况下仍然有效。即使没有lambda,该问题也会重现。这使我认为可能存在JVM错误。

在http://codingdict.com/questions/122889的评论中,我试图找出原因,导致代码行为从一个起点到另一个起点有所不同,而该讨论的参与者为我提供了一个建议,以创建一个单独的主题。

让我们考虑以下源代码:

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

这看起来像经典的初始化死锁,但是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尽管该类正在通过thread进行初始化,但不会被阻塞main

那其他JVM呢

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

怎么发生的

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

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

类似的bug对getstatic/ putstatic不久前固定-
JDK-4493560,但修复没有触及invokestatic。我已提交了新的错误JDK-8215634以解决此问题。

至于原来的例子

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

更新资料

该错误已确认。在即将发布的版本中已修复该问题:JDK 8u201,JDK 11.0.2和JDK 12。



 类似资料:
  • 我遇到了一个奇怪的情况,在静态初始化器中使用带有lambda的并行流似乎永远不会占用CPU。代码如下: 这似乎是该行为的最小再现测试用例。如果我: null 我使用的是OpenJDK版本1.8.0_66-internal。

  • 在使用Java并行流时,当一些并行操作在静态初始化器块内完成时,我遇到了死锁。 当并行处理流时,所有的工作(数字不按顺序显示): 但是,当使用处理流时,会出现死锁(我假设这与主线程和ForkJoinPool管理之间的交互有关): 但是当在一个单独的线程中生成流处理时,一切都很顺利: 如果能理解为什么在某些情况下会出现僵局,而在其他情况下却不会出现这种情况,我将不胜感激。这显然不仅仅是因为使用了静态

  • 我遇到了下面的Java代码,起初看起来不错,但从未编译过: 下面是IDE :变量USER_ID可能已分配的错误消息。 将值赋值给静态最终变量有问题吗?

  • 问题内容: 我有2节课: Class A: Class B: I create a Main class which just creates new A: The output I get is: 如您所见,A的构造函数在其静态初始值设定项之前被调用。 我了解它与我创建的循环依赖关系有关,但我印象中静态初始化程序应始终在构造函数之前运行。 发生这种情况的原因是什么(技术上在Java实现中)? 是

  • 最近我在写一些复杂的基于RX的流程,发现它总是在特定情况下产生死锁。我花了几个小时才找出问题所在,似乎可以在这个简单的示例中重现: 此程序应打印以下值:11、21、22、31、32、33、。。。,通常,值可以表示为XY。每组X中的值的顺序可以是随机的,但组的顺序应该是升序。如果previous仍在计算,则不应发出新组(这是我的原始情况)。 问题是,如果您运行这段代码,您将只看到前几个元素的输出-我

  • 报价王 JLS #8.1.3: 内部类不能声明静态初始值设定项 (§8.7)...... 这表现为: 现在既然Java的内部(非静态)类像其他类一样由类加载器加载,为什么我们不能为它们提供静态初始化器呢? 这一限制背后的原因是什么?