亮劍 | 義大利炮轟 Java多執行緒join方法?
有一天我走在路上,偶爾接觸到了Thread.join()方法,便到網上去查閱相關資料,但也是看得我一頭霧水(花露水)。在我很久的理解之後,便想將我理解的join()方法以一張圖的方式解釋出來。
一、我畫
開局一張圖:
我繪製的這張圖以時間線的形式解釋了主執行緒和各個被例項化的John
執行緒的執行過程。
彆著急,多看兩遍! 我們可以看到,在主執行緒呼叫了join()方法後,指定的執行緒會被抓回來在後邊老老實實地排隊。
二、你猜
? 問:請回答未執行join()方法和執行了join()方法的執行結果有什麼不同(可能有幾種結果、執行的時間、輸出的順序)?
? 答:
型別 | 可能結果數量 | 執行速度 | 輸出順序 | 時間複雜度(最好) |
---|---|---|---|---|
不使用join() | 6種 | 快(多執行緒 | 亂序 | O(1) |
使用join() | 1種 | 慢(單執行緒) | 順序 | O(n) |
- 看完後,你可能還會有些疑問:
? Q:為什麼不使用join()的可能結果數量是6種?輸出順序為什麼是亂序?
? A:由於多執行緒的原因,雖然start()順序執行了執行緒1/2/3,但可能由於各種原因,某個執行緒會搶先完成,從而造成了6種執行結果 ——
123
132
213
231
321
312
複製程式碼
- 多執行緒並不老實,並且難以(ye不是不可能)控制。
? Q:為什麼使用join()後會變慢?
? A:因為使用join()後,三個執行緒會按順序被趕到主執行緒去執行,這時候它們就不能夠同時運行了,只能一個一個地執行
三、動手
別偷懶,乘還年輕,開啟你的IDE,把下邊的程式碼貼上進去:
public class TestJoin {
public static void main(String[] args) {
John john1 = new John();
John john2 = new John();
John john3 = new John();
try {
john1.start();
john1.join();
john2.start();
john2.join();
john3.start();
john3.join();
} catch (InterruptedException IE) {
IE.printStackTrace();
}
}
}
class John extends Thread {
@Override
public void run() {
for (int i = 0; i < 2; i++)
{
try
{
Thread.sleep(500);
System.out.println("Current Thread: "
+ Thread.currentThread().getName());
}catch(Exception ex){
System.out.println("Exception has" +
" been caught" + ex);
}
System.out.println(i);
}
}
}
複製程式碼
執行結果:
Current Thread: Thread-0
0
Current Thread: Thread-0
1
Current Thread: Thread-1
0
Current Thread: Thread-1
1
Current Thread: Thread-2
0
Current Thread: Thread-2
1
複製程式碼
- 可以看到,苦逼的三個執行緒都被拉到了主執行緒順序執行。
現在,刪掉兩行程式碼:
john2.join();
john3.join();
複製程式碼
再次執行:
Current Thread: Thread-0
0
Current Thread: Thread-0
1
Current Thread: Thread-1
0
Current Thread: Thread-2
0
Current Thread: Thread-2
1
Current Thread: Thread-1
1
複製程式碼
- 可以看到,john2和john3兩個執行緒並未在主執行緒中執行,所以執行結果也發揮得比較自由,且執行時間也縮短了。
此處請注意,在使用join()方法之前,一定要先使用start()方法啟動執行緒。執行緒根本沒工作,那還咋拉過去?
原始碼分析:
/**
* Waits for this thread to die.
* 等待該執行緒死亡
*/
public final void join() throws InterruptedException {
join(0);
}
/**
* Waits at most {@code millis} milliseconds for this thread to die
* 最多等待code毫秒為該執行緒死亡,就是說如果該執行緒死亡時間小於該code則提前結束,繼續往下執行
* 如果該執行緒在code毫秒內沒有死亡,到時間了則不管該執行緒,繼續往下執行
*. A timeout of {@code 0} means to wait forever.
* 如果時間code=0則永久等待,直到該執行緒消亡
*/
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
複製程式碼
原始碼中:
- 就是說join()=join(0)
- 如果時間code millis=0的時候是無限等待,直到該執行緒消亡
- 如果時間是大於0的則該執行緒執行時間等於該時間後無論是否消亡,都將通知呼叫join方法的執行緒去繼續執行,即notifyAll
public static void main(String[] args) {
John john1 = new John();
John john2 = new John();
John john3 = new John();
try {
john1.start();
john1.join();
john2.start();
john2.join(5);
john3.start();
john3.join();
} catch (InterruptedException IE) {
IE.printStackTrace();
}
}
複製程式碼
執行結果
Current Thread: Thread-0
0
Current Thread: Thread-0
1
Current Thread: Thread-1
0
Current Thread: Thread-2
0
Current Thread: Thread-1
1
Current Thread: Thread-2
1
複製程式碼
- 可以看到,執行緒john2線上程被主執行緒加入進來5毫秒之後,執行緒3啟動,並且開始執行,join()的用處其實就是執行緒的協同,執行緒的有序執行,執行緒的節制於時間的排程~
四、結束
到此,你已經掌握了join()方法的使用。不要問我能用來做什麼,等到你需要它的功能時,你就不會再手忙腳亂了。
很認真,你讀到了最後。讓我們再來講講yield()方法吧(別的文章都講了):
yield()方法的作用,與join()方法無瓜。當你對一個執行緒執行yield()方法後,該執行緒會嘗試暫停自己,給後面正在排隊的同級執行緒讓道(即拱手讓人),它是一個不穩定的過程(不一定有效果,也不一定什麼時候繼續執行)。
另外,建議你趁著還能再學進點什麼,再看看syncronized、wait()、notify()這些有相似之處的知識比較好。
看圖譜我都給你找來了: