先給出一道很簡潔的小段 Java 程序,你看一下是否能答出正確結果。
在類中有一個靜態變量;
靜態方法塊中,拋出子線程修改變量的值,然後等待子線程執行結束;
main 方法查看變量的值。
public class LambdaTest {
// 靜態變量初始爲false
static boolean b = false;
static {
// 拋出子線程將變量改爲true
Thread t = new Thread(() -> b = true);
t.start();
// 等待子線程執行完成
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 輸出變量是否被修改
System.out.println(b);
}
}
最好不要先偷窺答案。
我們先想一想可能結果是什麼:
比如
- 修改成功,打印 true
- 修改不成功,打印 false
- 編譯錯誤(猜的)
- 程序卡死(我也不知道,你自己想)
- 程序拋出異常。。
也許還有其他答案吧,需要你們自己分析判斷。
接下來,就是見證奇蹟的時刻:
好吧,程序卡死了。
(
不知道你猜對了沒有。
假設你碰巧猜對了,
那我再對代碼稍作修改,你看一下是否還能繼續猜對
)
嘗試將 lambda 表達式改爲空
Thread t = new Thread(() -> {});
總代碼
public class LambdaTest {
// 靜態變量初始爲false
static boolean b = false;
static {
// 這次我什麼都不幹總可以了吧
Thread t = new Thread(() -> {});
t.start();
// 等待子線程執行完成
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 輸出變量
System.out.println(b);
}
}
不要懵逼,我再改一改
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("你幹嘛呢");
}
});
總代碼
public class AnonymousInnerClass {
static boolean b = false;
static {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("你幹嘛呢");
}
});
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
System.out.println(b);
}
}
不錯,執行完了
再來:
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子線程運行");
b = true;
}
});
完整代碼
public class AnonymousInnerClass {
static boolean b = false;
static {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子線程運行");
b = true;
}
});
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
System.out.println(b);
}
}
誒?
子線程運行了?
可是怎麼還是卡死了。。。。
不急,繼續
改成方法引用
// 改成方法引用
Thread t = new Thread(System.out::println);
完整代碼
public class MethodQuote {
static boolean b = false;
static {
Thread t = new Thread(System.out::println);
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
System.out.println(b);
}
}
它又奇怪的執行結束了。。。
(當然全答出來的大佬就不要來吐槽我了)
解析
經過百般波折,你們可能已經蒙了。
這其中主要是涉及到
- 匿名內部類
- lambda 表達式
- 方法引用
我們發現,這三種方式,拋出的線程,執行的結果狀態都不一樣。
可能很多人想當然的認爲,lambda 表達式,和匿名內部類是同一種實現,只是寫法不同。
然而實際上不是的(從這裏面也可以體現出)。
下面我們一步步來分析。
爲了方便定位,我把 new 出來的子線程命了一個名字:叫子線程
然後我們打印出線程信息,發現,子線程還是 RUNNABLE 的狀態。
但是,我們仔細一看:
cpu=0.00ms
說明它根本就還沒有執行 !!!
然後我們再轉頭一看主線程,發現它已經陷入了 WAITING 中,
也就是說,我們的程序已經被卡死了。
爲了理清到底是哪些地方出了問題,我們在程序的各個點打印輸出,來進行錯誤排查
public class LambdaTest {
static {
System.out.println(Thread.currentThread().getName() + ":static方法開始");
Thread t = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ":子線程運行");
}, "子線程");
t.start();
try {
System.out.println(Thread.currentThread().getName() + ":子線程join");
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":static方法結束");
}
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + ":main方法運行");
}
}
- 我們可以發現,類的初始化是由 main 線程執行的。
- 然後,程序運行到 join 之後,整個程序卡死。
- 我們之前也看過,main 線程是處於 WAITING 狀態的
所以肯定是運行的子線程哪裏有問題。
我們點開 join 方法,也可以發現,join 的本質也就是 wait
我們 debug 一下,會發現線程在執行結束後,最終會退出,喚醒那些 join 了自己的線程
所以我們就要去弄明白,爲什麼子線程沒有成功執行。
這就涉及到 lambda 表達式 和 匿名內部類 之間的區別了。
我們先看匿名內部類:
我們打開編譯過後的 class 所在的文件夾,打開過後,發現有兩個 class 文件,
一個是我們之前寫的測試類;
還有一個就是我們的匿名內部類,它的名字是在所在的類的名字後面加 $1
如果還有其它匿名內部類的話,就依次是 $1、$2、$3、$4……
也就是說,我們的匿名內部類,在編譯的時候,就已經存在了。
但是我們的 lambda表達式、方法引用,在編譯的時候不會產生類,而是運行時動態產生的。
(因爲我們編譯過後沒有產生其它 class)
我們目前已經開始發現一點區別了。
一個是靜態生成類,編譯時已經存在;
一個是動態生成類,只有在運行時纔會生成類。
我們繼續探究,
點開編譯後 class 的指令,我們可以發現:
在匿名內部類中,有 invokespecial 字節碼指令。
我們翻開官方文檔:
上面說明了這是一個初始化的指令。
其實我們在看到 class 字節碼指令的時候,後面寫了 init,我們也就能想出來了。
官方說明是爲了確認。
但是到了我們的 lambda 表達式的時候,我們可以看到:
在 class 字節碼中表示的是 invokedynamic 動態指令
區別顯而易見,匿名內部類由於編譯時已經產生,所以完全可以直接初始化,從而創建的線程可以正確執行匿名內部類的方法。
不過我們之前的題目中有一個示例,匿名內部類也無法成功執行,我們來看一下這又是爲什麼:
剛纔我們探討過了,採用匿名內部類是可以成功執行的,因爲在編譯時,類文件已經產生。
但是這裏,我們發現了一個點,該匿名內部類引用了我們外部類的靜態變量。
那我現在我們來分析程序執行流程。
- 首先,程序創建了匿名內部類的對象,然後創建子線程去跑這個 Runnable 對象。
- 線程啓動後,開始執行,主線程調用 join 等待子線程結束
- 子線程成功打印了 “子線程運行” 這句話(我們之前的執行結果便是如此)
- 但是子線程訪問到 外部類的變量
這就是我們要注意的點了,我們要去訪問一個類的時候,這個類必須是初始化完成的 !!!
(按照道理我們不可以去訪問才初始化一半的對象,這時候 static 方法都沒執行完)
所以,子線程此刻便在等待,主線程完成外部類的初始化;
但是,這時候主線程還在等子線程執行完。。。。
所以,就產生了死鎖。
所以,不管是對於匿名內部類,還是其他任意的一個類,只要去訪問其他類,就必須等到那個類完成初始化。
這也就是我們在類中訪問了外部類變量而導致死鎖的原因。
我們繼續回到 lambda 表達式,我們之前已經發現,它在編譯的時候,根本沒有創建出類。
也就是類是運行時生成。
這時候,我們不管有沒有在類的方法中引用外部類的變量,都沒有什麼實質性的作用。
因爲這時候要動態生成內部類,就必須先初始化外部類。
這時候,我們已經區分好了匿名內部類和 lambda 表達式的區別,雖然都是生成類,但是:
- 匿名內部類是編譯時靜態生成
- lambda 表達式是運行時動態生成類
但是,我們可別忘了,我們還有一個方法引用沒有講
我們記得當時的代碼示例,在方法引用的時候,程序是可以成功執行結束的。
但是我們點開一看,發現卻是 invokedynamic
怎麼感覺我在啪啪打臉??
不怕,我們繼續點進去看一下。。
我們可以發現,由於 Runnable 引用的就是一個 pringln 方法,
而 System.out 對象,早就以及初始化完成了,所以根本不用去擔心初始化內部類而要先初始化外部類的問題。
因而,採取方法引用的方式,也可以執行成功。
總結
現在,想必你對匿名內部類、lambda 表達式、方法引用,都有一定理解了。
其實,對於這些 Java 的知識,不僅僅只是簡單使用即可。
更多的時候,要去弄明白其中的原由,仍需要對一些源碼的掌握,以及對 Java 虛擬機的理解,操作系統底層的一些瞭解方可。
因爲很多時候,代碼的邏輯是確實不存在問題的,但是,對於一些底層的原理,會或多或少的影響到我們程序的執行。
因此,爲了超越一個 CRUD 程序員,我們必須去對知識的橫向、縱向擴展。
這樣,才能掌握徹底,理解深刻,方能運用自如。