你:Bug? Oracle:Feature!
问题现象
事情是这样的,有天晚上炜神在群里发了一张截图,一段代码让 static 代码块卡死了:
这段代码里都是 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,一直阻塞着,属实卡死了:
尝试去掉 .parallel()
再执,正常执行没问题,基本可以确定是并行流造成的,并行流是基于 ForkJoinPool 实现,本质上还是要靠多线程,而 static 代码块又是线程安全的,所以问题锁定在了这一块。
类初始化过程
先弄清楚类初始化是怎么保证线程安全的,Oracle 有官方文档:点击查看,详细的流程如图:
挑几条主要的,对于每个类或接口 C,都有一个唯一的初始化锁 LC,初始化 C 的关键的过程如下:
- 同步地获取 C 的初始化锁 LC,除非获取到否则一直等待
- 如果没有初始化,标记当前 C 的 Class 为正在初始化,然后释放 LC,进行初始化
- 如果当前有其他线程正在对 C 进行初始化,则释放 LC 并阻塞当前线程,直到初始化已完成被通知解除阻塞
- 如果是当前线程正在对 C 进行初始化,释放 LC 并正常完成初始化
- 如果 C 已经被初始化,则不需要进一步的操作,释放 LC
- 如果初始化正常执行完成,则获取 LC,将 C 的 Class 标记为已初始化完成,通知所有等待线程,释放 LC
- 当可以确定类的初始化已经完成时,可以通过省略步骤 1 中的锁获取和之后的锁释放来优化此过程
问题分析
由类的初始化流程可见,涉及到多线程地方为关键流程的第 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
被编译成了一个静态方法:
实际效果相当于执行下面的代码,执行一下,同样地也会卡死:
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");
}
}
- 执行 main 方法时,触发类的初始化,执行 static 代码块,此时 main 线程获取到 LC,将当前类的状态标记为正在初始化,然后释放 LC
- 其他线程执行该类的静态方法时,获取到 LC,发现该类状态为正在初始化,释放掉 LC 并阻塞当前线程,直到 main 线程类初始化完成后,该线程才能被通知解除阻塞
- 在 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");
}
}
正常运行,其他线程未触发初始化
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");
}
}