一道题的思考

题目

在小马哥的每日一问中看到了一道这个题:输出什么?。当时看错了在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: 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的匿名内置类的方式:

 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方法是个空方法体,即代码为:

   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,即代码为:

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值