再深一点,理解线程的join方法

孤者浪人 提交于 2019-12-09 11:11:11

配图:曲径通幽

 

讲真,如果不是被面试官吊打,join()方法也还不会引起我的重视。因为,工作中确实没有使用过它。

现在,对它来个刨根问底。

join()方法的作用

在写这篇文章之前,我对join的理解只停留在字面意思“把指定线程加入到当前线程”。

再来看官方怎么解释的:

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

“Waits for this thread to die.”,也就是等着join()方法所属的 线程死亡(run方法执行完毕正常结束或线程异常死亡)。

下面举个例子,证实这个说法。

举个栗子

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();
       }
   }
}

 ​​输出结果:

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方法的一个很有力的应用案例。感兴趣自己研究一下吧。

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!