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

2022-08-31 11:25:25

我遇到了一个奇怪的情况,在静态初始值设定项中使用带有lambda的并行流似乎永远没有CPU利用率。代码如下:

class Deadlock {
    static {
        IntStream.range(0, 10000).parallel().map(i -> i).count();
        System.out.println("done");
    }
    public static void main(final String[] args) {}
}

这似乎是此行为的最小重现测试用例。如果我:

  • 将块放在 main 方法中,而不是静态初始值设定项中,
  • 删除并行化,或
  • 删除 lambda,

代码立即完成。任何人都可以解释这种行为吗?这是一个错误还是有意为之?

我正在使用OpenJDK版本1.8.0_66-内部。


答案 1

我发现一个非常相似的案例(JDK-8143380)的错误报告,该案例被Stuart Marks关闭为“不是问题”:

这是一个类初始化死锁。测试程序的主线程执行类静态初始值设定项,该初始值设定项为类设置初始化进行中标志;此标志将保持设置状态,直到静态初始值设定项完成。静态初始值设定项执行并行流,这会导致在其他线程中计算 lambda 表达式。这些线程阻塞等待类完成初始化。但是,主线程被阻塞,等待并行任务完成,从而导致死锁。

应更改测试程序,以将并行流逻辑移到类静态初始值设定项之外。关闭为“不是问题”。


我能够找到另一个错误报告(JDK-8136753),也由Stuart Marks以“Not a Issue”关闭:

这是一个正在发生的死锁,因为 Fruit 枚举的静态初始值设定项与类初始化交互不良。

有关类初始化的详细信息,请参见 Java 语言规范第 12.4.2 节。

http://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.4.2

简而言之,正在发生的事情如下。

  1. 主线程引用 Fruit 类并启动初始化过程。这将设置初始化进行中标志,并在主线程上运行静态初始值设定项。
  2. 静态初始值设定项在另一个线程中运行一些代码,并等待其完成。此示例使用并行流,但这与流本身无关。通过任何方式在另一个线程中执行代码并等待该代码完成,将具有相同的效果。
  3. 另一个线程中的代码引用 Fruit 类,该类检查初始化正在进行的标志。这会导致另一个线程阻塞,直到清除该标志。(请参阅 JLS 12.4.2 的步骤 2。
  4. 主线程被阻塞,等待另一个线程终止,因此静态初始值设定项永远不会完成。由于初始化进行中标志在静态初始值设定项完成之前不会清除,因此线程将死锁。

若要避免此问题,请确保类的静态初始化快速完成,而不会导致其他线程执行要求此类完成初始化的代码。

关闭为“不是问题”。


请注意,FindBugs 在为这种情况添加警告时存在一个未解决的问题


答案 2

对于那些想知道引用类本身的其他线程在哪里的人来说,Java lambdas的行为就像你这样写的:Deadlock

public class Deadlock {
    public static int lambda1(int i) {
        return i;
    }
    static {
        IntStream.range(0, 10000).parallel().map(new IntUnaryOperator() {
            @Override
            public int applyAsInt(int operand) {
                return lambda1(operand);
            }
        }).count();
        System.out.println("done");
    }
    public static void main(final String[] args) {}
}

使用常规的匿名类,没有死锁:

public class Deadlock {
    static {
        IntStream.range(0, 10000).parallel().map(new IntUnaryOperator() {
            @Override
            public int applyAsInt(int operand) {
                return operand;
            }
        }).count();
        System.out.println("done");
    }
    public static void main(final String[] args) {}
}

推荐