吊打面试官之Thread join的原理和使用剖析

官方解释thread join

很多人对Thread.join的作用以及实现了解得很少,毕竟这个api我们很少使用。这篇文章仍然会结合使用及原理进行深度分析。

void join() 等待这个线程死亡。
void join(long millis) 等待这个线程死亡最多 millis毫秒。

我们可以看到这样的解释还是有点不准确,大白话其实就等待一个线程从一个RUNNABLE状态到线程运行结束。

Thread 面试

Java中如何让多个线程按照自己指定的顺序执行?

这个问题最简单的回答是通过Thread.join来实现,但是这样就有会有一个问题,时间久了就让很多人误以为Thread.join是用来保证线程的顺序性的。下面这段代码演示了Thread.join的作用
本工程是用maven构建

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.changhong.threadtest</groupId>
    <artifactId>thread-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>

        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.8</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.21</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.1.7</version>
        </dependency>

        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.1.7</version>
        </dependency>

    </dependencies>


</project>

这个例子就是等待 join-first-thread 2秒后线程结束了,在执行后面的join-second-thread 线程。

package com.changhong.thread.chapter1;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
@Slf4j
public class ThreadJoinTest {
    public static void main(String[] args) {
        try {
            TimeUnit.SECONDS.sleep(1);
            final Thread first=new Thread(new Runnable() {
                public void run() {
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        log.error("current thread run focus something wrong , messages is [{}]",e.toString());
                    }
                    log.error("this is a job-num-0");
                }
            },"join-first-thread");
            first.start();
            final Thread second=new Thread(new Runnable() {
                public void run() {
                    try {
                        first.join();
                    } catch (InterruptedException e) {
                        log.error("current thread run focus something wrong , messages is [{}]",e.toString());
                    }
                    log.error("this is a job-num-1");
                }
            },"join-second-thread");
            second.start();
        } catch (InterruptedException e) {
            log.error("the program have error focus [{}]",e.toString());
        }
    }
}

控制台打印的结果如下:

2019-09-13 18:40:52.714 [join-first-thread] ERROR com.changhong.thread.chapter1.ThreadJoinTest - this is a job-num-0
2019-09-13 18:40:52.716 [join-second-thread] ERROR com.changhong.thread.chapter1.ThreadJoinTest - this is a job-num-1

Thread.join的实现原理

线程是如何被阻塞的?又是通过如何唤醒的呢?先来看看JDK Thread.join的源码是如何实现的?

    /**
     * Waits for this thread to die.
     *
     * <p> An invocation of this method behaves in exactly the same
     * way as the invocation
     *
     * <blockquote>
     * {@linkplain #join(long) join}{@code (0)}
     * </blockquote>
     *
     * @throws  InterruptedException
     *          if any thread has interrupted the current thread. The
     *          <i>interrupted status</i> of the current thread is
     *          cleared when this exception is thrown.
     */
    public final void join() throws InterruptedException {
        join(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;
            }
        }
    }

从jdk join方法的源码来看,join方法的本质调用的是Object中的wait方法实现线程的阻塞。但是我们知道,调用wait方法必须要获取锁,所以join方法是被synchronized修饰的,synchronized修饰在方法层面相当于synchronized(this),this就是join-first-thread本身的实例。

有很多人不理解join为什么阻塞的是join-first-thread线程呢? 不理解的原因是阻塞join-second-thread线程的方法是放在join-first-thread这个实例作用,让大家误以为应该阻塞join-first-thread线程。实际上join-second-thread线程会持有join-first-thread这个对象的锁,然后调用wait方法去阻塞,而这个方法的调用者是在join-second-thread线程中的。所以造成主线程阻塞。

第二个问题,为什么join-first-thread线程执行完毕就能够唤醒第二个线程呢?或者说是在什么时候唤醒的?
我们打开 Thread类的源码如下:

public
class Thread implements Runnable {
    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }

    private volatile String name;
    private int            priority;
    private Thread         threadQ;
    private long           eetop;

    /* Whether or not to single_step this thread. */
    private boolean     single_step;
  .......省略代码

我们可以看到这里有一个本地注册方法的代码 registerNatives();
这个方法放在一个static语句块中,当该类被加载到JVM中的时候,它就会被调用,进而注册相应的本地方法。而本地方法registerNatives()是定义在Thread.c文件中的。

static JNINativeMethod methods[] = {
      {"start0", "()V",(void *)&JVM_StartThread},
      {"stop0", "(" OBJ ")V", (void *)&JVM_StopThread}, 
      {"isAlive","()Z",(void *)&JVM_IsThreadAlive}, 
      {"suspend0","()V",(void *)&JVM_SuspendThread},
      {"resume0","()V",(void *)&JVM_ResumeThread}, 
      {"setPriority0","(I)V",(void *)&JVM_SetThreadPriority}, 
      {"yield", "()V",(void *)&JVM_Yield}, 
      {"sleep","(J)V",(void *)&JVM_Sleep}, 
      {"currentThread","()" THD,(void *)&JVM_CurrentThread}, 
      {"countStackFrames","()I",(void *)&JVM_CountStackFrames}, 
      {"interrupt0","()V",(void *)&JVM_Interrupt}, 
      {"isInterrupted","(Z)Z",(void *)&JVM_IsInterrupted},
      {"holdsLock","(" OBJ ")Z",(void *)&JVM_HoldsLock}, 
      {"getThreads","()[" THD,(void *)&JVM_GetAllThreads}, 
      {"dumpThreads","([" THD ")[[" STE, (void *)&JVM_DumpThreads}
};

如果想要彻底的分析这个这个问题,我们必须找到jdk的源码,但是如果大家对线程有一定的基本了解的话,通过wait方法阻塞的线程,需要通过notify或者notifyall来唤醒。所以在线程执行完毕以后会有一个唤醒的操作,只是我们不需要关心。接下来在hotspot的源码中找到 thread.cpp,看看线程退出以后有没有做相关的事情来证明我们的猜想。

if (millis == 0) {
        	//直到该线程死亡才结束
            while (isAlive()) {
                wait(0);
            }
        } 

我们看到thread.cpp的代码有这样的逻辑。

static void ensure_join(JavaThread* thread) {
  // We do not need to grap the Threads_lock, since we are operating on ourself.
  Handle threadObj(thread, thread->threadObj());
  assert(threadObj.not_null(), "java thread object must exist");
  ObjectLocker lock(threadObj, thread);
  // Ignore pending exception (ThreadDeath), since we are exiting anyway
  thread->clear_pending_exception();
  // Thread is exiting. So set thread_status field in  java.lang.Thread class to TERMINATED.
  java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
  
  // Clear the native thread instance - this makes isAlive return false and allows the join()
  // to complete once we've done the notify_all below
  //如果发现线程结束,就return fasle
  
  
  {  这个是java代码里面的逻辑
  if (millis == 0) {
        	//直到该线程死亡才结束
            while (isAlive()) {
                wait(0);
            }
        } 
  }

  java_lang_Thread::set_thread(threadObj(), NULL);
  lock.notify_all(thread);
  // Ignore pending exception (ThreadDeath), since we are exiting anyway
  thread->clear_pending_exception();
}


从这个源码可以看到ensure_join方法中,调用 lock.notify_all(thread); 唤醒所有等待thread锁的线程,意味着调用了join方法被阻塞的线程会被唤醒,到目前为止,我们基本上对join的原理做了一个比较详细的分析。

总结
1,Thread.join其实底层是通过wait=notifyall来实现线程通信达到线程阻塞
2,当线程执行结束以后,java_lang_Thread::set_thread(threadObj(), NULL); 调用这个设置native线程对象为null,lock.notify_all(thread);让等待在对象锁上的wait方法被唤醒。

如果有兴趣的人可以下载 Thread.cpp的源码研究

该博客为独秀天狼原创,转载请注明出处。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章