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


    配图:曲径通幽

    讲真,如果不是被面试官吊打,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
    ", 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方法的一个很有力的应用案例。感兴趣自己研究一下吧。

  • 相关阅读:
    pandas 筛选指定行或者列的数据
    数据相关性分析方法
    导入sklearn 报错,找不到相关模块
    特征探索经验
    python 中hive 取日期时间的方法
    云从科技 OCR任务 pixel-anchor 方法
    五种实现左中右自适应布局方法
    vscode vue 代码提示
    js Object.create 初探
    webpack 加载css 相对路径 ~
  • 原文地址:https://www.cnblogs.com/lukeguo/p/8824766.html
Copyright © 2020-2023  润新知