吊打面試官之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的源碼研究

該博客爲獨秀天狼原創,轉載請註明出處。

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