题目
在小马哥的每日一问中看到了一道这个题:输出什么?。当时看错了在static块中的代码,就毫不意外的答错了= =,这个题其实没有看起来那么简单,这里去记录下这个题。小马哥这个每日一题的系列有很多比较”坑”的题,一般第一遍都比较难答对,推荐每天没事的时候可以去思否上看看这个题,也算拾遗一些基础~
再来看看这个问题的代码:
1 | public class Lazy { |
这个题问的是最后输出的什么。一开始很想当然的就去想输出什么,但是最后在ide中试了下运行,发现启动就卡在了那里_(:з」∠)…
后面就去用jstack看了下线程的情况:
1 | 2019-08-03 20:23:45 |
发现Thread-0是Runnable状态的,但是是in object.wait() 这里还是卡住了没有执行。
思考
这个题里有几个点:
(1)static块也是main线程去加载的
(2)匿名内置类和lambda是有区别的
这里去简单说明下,如果在线程中用的是new Runnable的匿名内置类的方式:
1 | static { |
也就是在看编译生成的字节码目录中会多一个Lazy$1.class文件:
并且在反编译Lazy中看到static块中,依赖这个Lazy$1.class的init方法。
而如果是使用的是像题目中的lambda表达式方式,可以看到字节码文件中并没有Lazy$1.class,而是在反编译class文件中的字节码中多了invokeDynamic指令来实现的lambda表达式:
如果是匿名内之类的方式
我们先看如果是换成Runnable匿名内置类方式,而实现的run方法是个空方法体,即代码为:
1 | private static boolean initialized = false; |
这时启动并不会hang住;将run方法中加入了对static变量initialized的修改或者调用private static方法println,即代码为:
1 |
|
再次启动,会发现也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 | static { |
但是启动就不会有问题,因为这个等待的是java.io.PrintStream这和类初始化,而这个类初始化是BootStrap类加载器初始化的,早于Lazy类初始化加载,所以能正常运行。
也就是说,在static代码块中:
- 当使用匿名内置类的时候,注意不要依赖外部类的静态变量或者方法
- 当使用lambda表达式或者方法引用,注意类的加载的先后顺序,如果依赖不当,会造成启动死锁的情况。