題目
在小馬哥的每日一問中看到了一道這個題:輸出什麼?。當時看錯了在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表達式或者方法引用,注意類的加載的先後順序,如果依賴不當,會造成啓動死鎖的情況。