一道題的思考

題目

在小馬哥的每日一問中看到了一道這個題:輸出什麼?。當時看錯了在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表達式或者方法引用,注意類的加載的先後順序,如果依賴不當,會造成啓動死鎖的情況。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章