1. 程式人生 > 程式設計 >亮劍 | 義大利炮轟 Java多執行緒join方法?

亮劍 | 義大利炮轟 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;
            }
        }
    }
複製程式碼

原始碼中:

  1. 就是說join()=join(0)
  2. 如果時間code millis=0的時候是無限等待,直到該執行緒消亡
  3. 如果時間是大於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()這些有相似之處的知識比較好。

看圖譜我都給你找來了: