再深一點,理解線程的join方法

配圖:曲徑通幽java

 

講真,若是不是被面試官吊打,join()方法也還不會引發個人重視。由於,工做中確實沒有使用過它。面試

如今,對它來個刨根問底。c#

join()方法的做用多線程

在寫這篇文章以前,我對join的理解只停留在字面意思「把指定線程加入到當前線程」。併發

再來看官方怎麼解釋的:app

//Waits for this thread to die.
public final void join() throws InterruptedException {
   join(0);
}

「Waits for this thread to die.」,也就是等着join()方法所屬的 線程死亡(run方法執行完畢正常結束或線程異常死亡)。框架

下面舉個例子,證明這個說法。jvm

舉個栗子this

import  static  java.lang.System.out;
public class JoinTest {
   public static void main(String args[]) throws InterruptedException{
       String threadName = Thread.currentThread().getName();
       out.println(threadName + " is Started");
       Thread th1 = new FooThread();
       th1.start();
       th1.join();
       out.println(threadName + " is Completed");
   }
}

public class FooThread extends  Thread{
   public void run(){
       try {
           String threadName = Thread.currentThread().getName();
           out.println(threadName + " is Started");
           Thread.sleep(2000);
           out.println(threadName + " is Completed");
       } catch (InterruptedException ex) {
           ex.printStackTrace();
       }
   }
}

 ​​輸出結果:google

main is Started
Thread-0 is Started
Thread-0 is Completed
main is Completed

例子中,首先主線程main thread開始執行,接着主線程建立並啓動了另一個線程「Thread-0」。由於Thread-0睡了2秒,因此線程Thread-0至少須要2秒才能執行完成。通常情景,主線程啓動線程Thread-0後,會繼續本身的工做,而不關心線程Thread-0的執行狀況。可是因爲join()方法的調用,主線程必須等待,直到Thread-0執行完成,主線程才能夠繼續執行後面的代碼。這個執行順序經過輸出結果也能夠看出。

也能夠說線程Thread-0加入了正在執行的主線程,這樣理解更貼切方法名join。

繼續看源代碼{join(0)}方法的實現。

實現原理

/*
* Waits at most millis milliseconds for this thread to die.
* A timeout of 0 means to wait forever.
*/
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) {//join(0)
           while (isAlive()) {
               wait(0);
           }
       } else {
           while (isAlive()) {
               long delay = millis - now;
               if (delay <= 0) {
                   break;
               }
               wait(delay);
               now = System.currentTimeMillis() - base;
           }
       }
}

註釋的意思是,等待這個線程最多millis毫秒,millis毫秒後,無論這個線程有沒有死亡,主線程繼續執行。

超時時間爲0,意味着主線程要永遠等待。

進入{if(millis === 0)}條件模塊後,首先根據isAlive()判斷這個線程(例子中的Thread-0線程)是否還活着,若是活着,阻塞主線程(例子中的main線程)。把{wait(0)}放在while循環中,是爲了防止主線程阻塞期間被其餘線程喚醒。

也就說,此時主線程main會被一直阻塞,直到Thread-0線程執行結束。

But !

Thread-0執行結束後,即whie(isAlive)返回false時,join方法隨之也就結束了。並無看到喚醒主線程的代碼?

其實join方法的註釋中還有這樣一句話:  

「As a thread terminates the this.notifyAll method is invoked. 
It is recommended that applications not use wait, notify, or notifyAll  on Thread instances.」

當一個線程結束的時候,會主動調用{this.notifyAll}喚醒全部等待該線程對象鎖(例子中的th1)的全部線程。而且,不建議在應用程序中調用該線程對象的wait,notify,notifyAll方法。

看完這句話,前一秒豁然開朗;後一秒,我仍是想問「notifyAll在哪調用的,仍是沒看到?」

帶着這個問題我google了一下,找到了答案。答案說這段代碼在jvm code中,並無貼出具體代碼。

本身找唄。

JVM源碼

我並無下載HotSpot的代碼,由於龐大且複雜,想找到目標代碼不容易。而是下載了超小型Java虛擬機JamVM。有多小?HotSpot源代碼一百多兆,JamVM只有656kb,你感覺下?(表情:苦笑、苦笑)

麻雀雖小五臟俱全,JamVM的目標是支持最新版的Java虛擬機規範。研究JVM原理,它是個不錯的入門選擇。

thread.c#threadStart(void *arg)

threadStart負責初始化並執行線程的run方法

void *threadStart(void *arg){

   Thread *thread = (Thread *)arg;
   Object *jThread = thread->ee->thread;

   enableSuspend(thread);

  //初始化線程結構體,建立線程棧等
   initThread(thread, INST_DATA(jThread, int, daemon_offset), &thread);

   /* Add thread to thread ID map hash table. */
   addThreadToHash(thread);

   /* 執行線程的run方法 */
   executeMethod(jThread, CLASS_CB(jThread->class)->method_table[run_mtbl_idx]);

   /* run方法執行完畢。分離線程並退出 */
   detachThread(thread);

   TRACE("Thread 0x%x id: %d exited\n", thread, thread->id);
   return NULL;
}

能夠看到,detachThread方法負責善後線程執行結束後的工做。

thread.c#detachThread(Thread *thread)

void detachThread(Thread *thread) {
   //省略...
   /* Remove thread from the ID map hash table */
   deleteThreadFromHash(thread);

   /* 喚醒全部等待VMThread對象的線程 */
   objectLock(vmthread);
   objectNotifyAll(vmthread);
   objectUnlock(vmthread);

   /* Disable suspend to protect lock operation */
   disableSuspend(thread);
   /* 從Thread鏈表中刪除 */
   if((thread->prev->next = thread->next))
       thread->next->prev = thread->prev;
   /* 線程數減一 */
   threads_count--;
   /* 回收線程ID */
   freeThreadID(thread->id);
   /* 釋放系統資源*/
  sysFree(ee->stack);
   sysFree(ee);
   //省略...
}

在善後工做中,首先要作的就是喚醒全部在等待該線程對象的線程,而後是回收系統資源等。

再談join的應用

join方法能夠實現讓多線程按指定順序執行,這點在須要多線程相互協做工做的業務場景中很重要。

需求:計算1+2+3+...+100的結果。

爲了提升計算速度,咱們啓動兩個線程並行計算,線程leftThread計算1到50的和,線程rightThread計算51到100的和。

線程sumThread負責合併最後的計算結果,因此線程sunThread必須等待leftThread和rightThread執行結束後,才能計算最後的結果,這裏就須要把兩個計算線程join到sumThread線程中。sumThread的run方法以下:

public void run(){
   int leftResult = leftThread.join();
   int rightResult = rightThread.join();
   sum = leftResult + right Result;
}

But !

有個問題,join方法是不能返回線程的計算結果的。怎麼辦?

幸運的是,JDK中爲咱們提供了現成的解決方案。在jdk7中,concurrent包的做者Doug Lea給咱們帶來了一個高效的並行計算框架Fork/Join。Fork/Join模式極大的簡化了開發併發程序的繁瑣工做。But! 這個框架不是這篇文章的重點,之因此提到它是由於這是join方法的一個頗有力的應用案例。感興趣本身研究一下吧。

 

相關文章
相關標籤/搜索