你:Bug? Oracle:Feature!

问题现象

事情是这样的,有天晚上炜神在群里发了一张截图,一段代码让 static 代码块卡死了:

20220524-1

这段代码里都是 Stream 比较常见的操作符,用到了并行流 .parallel(),本地写了个并行流的 demo 跑了一下,果然会出现卡死的情况:

public class StaticTest {
  
    static {
        Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
                .parallel()
                .map(it -> it + 1)
                .collect(Collectors.toList());
    }
  
    public static void main(String[] args) {
        System.err.println("done");
    }
}

看了一下线程信息,确实 main 线程的状态变成了 WAITING,一直阻塞着,属实卡死了:

20220524-2

尝试去掉 .parallel() 再执,正常执行没问题,基本可以确定是并行流造成的,并行流是基于 ForkJoinPool 实现,本质上还是要靠多线程,而 static 代码块又是线程安全的,所以问题锁定在了这一块。

类初始化过程

先弄清楚类初始化是怎么保证线程安全的,Oracle 有官方文档:点击查看,详细的流程如图:

20220524-3

挑几条主要的,对于每个类或接口 C,都有一个唯一的初始化锁 LC,初始化 C 的关键的过程如下:

问题分析

由类的初始化流程可见,涉及到多线程地方为关键流程的第 3 点,出现了阻塞字眼,但第 3 点也提到了只有多个线程同时尝试初始化的才会出现阻塞,但下面这段代码怎么看怎么不像是会触发初始化的:

Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .parallel()
        .map(it -> it + 1)
        .collect(Collectors.toList());

于是看了一下这个 demo 类的 bytecode,发现了不太对劲的地方,.map 操作符里的 lambda it -> it + 1 被编译成了一个静态方法:

20220524-4

实际效果相当于执行下面的代码,执行一下,同样地也会卡死:

public class StaticTest {
  
    static {
        Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
                .parallel()
                .map(StaticTest::lambda$static$0)
                .collect(Collectors.toList());
    }

    private static Integer lambda$static$0(Integer integer) {
        return integer + 1;
    }
  
    public static void main(String[] args) {
        System.err.println("done");
    }
}

所以问题就变成了:在类的初始化的时候多线程执行正在该类的静态方法会卡死。

做一个实验如下,.map 操作符内不使用 lambda,而是使用 new 出来的匿名实现类,如图:

Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .parallel()
        .map(new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer integer) {
                return integer + 1;
            }
        })
        .collect(Collectors.toList());

执行代码,发现不卡死了,原因确实是上面所说的“如果当前有其他线程正在对 C 进行初始化,则释放 LC 并阻塞当前线程,直到初始化已完成被通知解除阻塞”,只不过阻塞后一直没被通知解除阻塞。

验证一下,执行类的静态方法会触发类初始化,在 Main 中执行 Test.test()

public class Test {

    static {
        System.out.println("执行static代码块");
    }

    public static void test() {
        System.out.println("执行test方法");
    }
}

public class Main {

    public static void main(String[] args) {
        Test.test();
    }
}

结果如下:

执行static代码块
执行test方法

问题原因

public class StaticTest {
  
    static {
        Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
                .parallel()
                .map(it -> it + 1)
                .collect(Collectors.toList());
    }
  
    public static void main(String[] args) {
        System.err.println("done");
    }
}
  1. 执行 main 方法时,触发类的初始化,执行 static 代码块,此时 main 线程获取到 LC,将当前类的状态标记为正在初始化,然后释放 LC
  2. 其他线程执行该类的静态方法时,获取到 LC,发现该类状态为正在初始化,释放掉 LC 并阻塞当前线程,直到 main 线程类初始化完成后,该线程才能被通知解除阻塞
  3. 在 main 线程中 static 代码块中的逻辑为等待其他线程执行完后才完成初始化,于是产生了死锁

问题复现

换一些比较直观点的代码

死锁,lambda 被编译成类静态方法,其他线程触发初始化类

public class StaticTest {

    static {
        Thread t = new Thread(() -> {}, "my-thread");
        t.start();
        //等待t线程执行完成
        t.join();
    }

    public static void main(String[] args) {
        System.err.println("done");
    }
}

20220524-5

正常运行,其他线程未触发初始化

public class StaticTest {

    static {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
            }
        }, "my-thread");
        t.start();
        //等待t线程执行完成
        t.join();
    }

    public static void main(String[] args) {
        System.err.println("done");
    }
}

死锁,在其他线程执行类的静态方法,触发初始化类

public class StaticTest {

    static {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                StaticTest.test();
            }
        }, "my-thread");
        t.start();
        //等待t线程执行完成
        t.join();
    }

    public static void test() {
    }
  
    public static void main(String[] args) {
        System.err.println("done");
    }
}