一道题的思考

题目

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

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

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看了下线程的情况:

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: RUNNABLEat 函数式设计.设计.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的匿名内置类的方式:

 static {println("static模块加载了");Thread t = new Thread(// new Runnable 匿名内置类是 通过 Lazy$1.class来实现的new Runnable() {@Overridepublic 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方法是个空方法体,即代码为:

   private static boolean initialized = false;// static也是由main线程去初始化的static {println("static模块加载了");Thread t = new Thread(// new Runnable 匿名内置类是 通过 Lazy$1.class来实现的new Runnable() {@Overridepublic 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,即代码为:

  @Overridepublic 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方法中使用的是代码:

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表达式或者方法引用,注意类的加载的先后顺序,如果依赖不当,会造成启动死锁的情况。


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部