Fork me on GitHub

一道题的思考

题目

在小马哥的每日一问中看到了一道这个题:输出什么?。当时看错了在static块中的代码,就毫不意外的答错了= =,这个题其实没有看起来那么简单,这里去记录下这个题。小马哥这个每日一题的系列有很多比较”坑”的题,一般第一遍都比较难答对,推荐每天没事的时候可以去思否上看看这个题,也算拾遗一些基础~

再来看看这个问题的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Lazy {

private static boolean initialized = false;

static {
Thread t = new Thread(() -> initialized = true);
t.start();
try {
t.join();
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}

public static void main(String[] args) {
System.out.println(initialized);
}
}

这个题问的是最后输出的什么。一开始很想当然的就去想输出什么,但是最后在ide中试了下运行,发现启动就卡在了那里_(:з」∠)…

后面就去用jstack看了下线程的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2019-08-03 20:23:45
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.171-b11 mixed mode):

"Thread-0" #10 prio=5 os_prio=31 tid=0x00007fece71eb800 nid=0x3d03 in Object.wait() [0x0000700005bbb000]
java.lang.Thread.State: RUNNABLE
at 函数式设计.设计.Lazy$$Lambda$1/495053715.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)


"main" #1 prio=5 os_prio=31 tid=0x00007fece6803800 nid=0x1703 in Object.wait() [0x0000700004c8e000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000007956ffb30> (a java.lang.Thread)
at java.lang.Thread.join(Thread.java:1252)
- locked <0x00000007956ffb30> (a java.lang.Thread)
at java.lang.Thread.join(Thread.java:1326)
at 函数式设计.设计.Lazy.<clinit>(Lazy.java:11)
JNI global references: 320

发现Thread-0是Runnable状态的,但是是in object.wait() 这里还是卡住了没有执行。

思考

这个题里有几个点:

(1)static块也是main线程去加载的

(2)匿名内置类和lambda是有区别的

这里去简单说明下,如果在线程中用的是new Runnable的匿名内置类的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static {
println("static模块加载了");

Thread t = new Thread(
// new Runnable 匿名内置类是 通过 Lazy$1.class来实现的
new Runnable() {
@Override
public void run() {

}
}

);
t.start();
try {
t.join();
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}

也就是在看编译生成的字节码目录中会多一个Lazy$1.class文件:

并且在反编译Lazy中看到static块中,依赖这个Lazy$1.class的init方法。

而如果是使用的是像题目中的lambda表达式方式,可以看到字节码文件中并没有Lazy$1.class,而是在反编译class文件中的字节码中多了invokeDynamic指令来实现的lambda表达式:

如果是匿名内之类的方式

我们先看如果是换成Runnable匿名内置类方式,而实现的run方法是个空方法体,即代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private static boolean initialized = false;

// static也是由main线程去初始化的
static {
println("static模块加载了");

Thread t = new Thread(
// new Runnable 匿名内置类是 通过 Lazy$1.class来实现的
new Runnable() {
@Override
public void run() {
System.out.println("匿名内置类执行");

}
}

);
t.start();
try {
t.join();
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}

public static void main(String[] args) {
println("main线程执行了");
System.out.println(initialized);
}

private static void println(Object o) {
System.out.printf("线程[%s]- %s\n", Thread.currentThread().getName(), o);
}

这时启动并不会hang住;将run方法中加入了对static变量initialized的修改或者调用private static方法println,即代码为:

1
2
3
4
5
6
7
  @Override
public void run() {
System.out.println("匿名内置类执行");
// 调用 static变量赋值或者static方法就会发生类似于死锁的现象 因为静态变量算这个类的一部分
initialized = true;
// println("static方法 打印线程名称执行");
}

再次启动,会发现也hang住出现死锁现象。

其实从上面三点就可以分析出,因为在static模块执行时(Lazy类是不完全初始化的),这时Runnable类也随之初始化,如果在Runnable类(也就是Lazy$1.class)初始化的时候,还依赖了Lazy的静态变量或者静态方法,那么就会产生字节码直接的循环依赖。

可以在下图中看到字节码中invokestatic指令代表依赖了Lazy的静态内容初始化完成:

再看回这道题

如果是lambda表达式,即使run方法中是空实现(即不在run方法中引用static变量或者static方法),启动也会hang住,这说明lambda来初始化线程并不受是否引用了static内容影响。

这里是因为 invokedDynamic指令是Lazy字节码的一部分,不需要因为引用static方法或者变量来执行,它需要等待Lazy类初始化的完成,而本身初始化完成又依赖invokedDynamaic指令的执行,同时执行的是字节码方法符为run:()Ljava/lang/Runnable,是执行自己的run方法,所以在字节码上也是一个循环依赖。(类加载器loadClass是同步的)。

这里注意下:这里不是只要用了invokeDynamic指令就会发生这个问题,比如方法引用也是通过invokeDynamic指令实现的如果在run方法中使用的是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static {
println("static模块加载了");

Thread t = new Thread(
// 方法引用
System.out::println

);
t.start();
try {
t.join();
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}

但是启动就不会有问题,因为这个等待的是java.io.PrintStream这和类初始化,而这个类初始化是BootStrap类加载器初始化的,早于Lazy类初始化加载,所以能正常运行。

也就是说,在static代码块中:

  • 当使用匿名内置类的时候,注意不要依赖外部类的静态变量或者方法
  • 当使用lambda表达式或者方法引用,注意类的加载的先后顺序,如果依赖不当,会造成启动死锁的情况。
-------------本文结束感谢您的阅读-------------

本文标题:一道题的思考

文章作者:夸克

发布时间:2019年07月22日 - 00:07

最后更新:2022年07月01日 - 06:07

原始链接:https://zhanglijun1217.github.io/2019/07/22/每日一问一道题的思考/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。