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

时间:2018-12-11 12:56:40

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

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

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

1 个答案:

答案 0 :(得分:11)

  

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并阻塞当前线程,直到得知正在进行的初始化已完成
  2.   

但是,尽管类Thread-2正在进行线程main的初始化,但-Xcomp不会被阻止。

其他JVM怎么样

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

它如何发生

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

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

getstatic / putstatic的类似错误已在很早之前得到修复-JDK-4493560,但该修复并未触及invokestatic。我已经提交了新的错误JDK-8215634,以解决此问题。

对于原始示例,

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

更新

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