Thread.join()源碼分析

掃描下方二維碼或者微信搜索公衆號菜鳥飛呀飛,便可關注微信公衆號,閱讀更多Spring源碼分析Java併發編程文章。java

微信公衆號

問題

  • 在join()方法中最終會調用到對象的wait()方法,而wait()方法一般是和notify()或者notifyAll()方法成對出現的。而在使用join()方法時,咱們壓根就沒有寫notify()或者notifyAll()方法,這是爲何呢?
  • 若是你知道答案,那麼本文將對你沒有任何幫助,你能夠直接跳過本文。

前言

  • 在前面兩篇文章中分析了CyclicBarrier和CountDownLatch的用法和使用原理,它們都是用來控制線程的執行順序的,這兩個類是JUC包下提供的兩個工具類,是由併發大佬Doug Lea開發的。實際上在Java語言當中,也提供了相關的API用來控制線程的執行順序,那就是Thread類中的join()方法,它是由Sun公司(現被Oracle收購)的開發人員開發的。

如何使用

  • join()方法的做用是:在當前線程A中調用另一個線程B的join()方法後,會讓當前線程A阻塞,直到線程B的邏輯執行完成,A線程纔會解阻塞,而後繼續執行本身的業務邏輯。能夠經過以下Demo示例感覺下其用法。
  • 在Demo示例中,」main線程「由於肚子餓了想吃飯,所以讓」保姆(線程thread)「 去作飯,只有飯作好了才能開始吃飯,所以」main線程「須要等待」保姆(線程thread)「 徹底執行完了(飯作好了)才能開始吃飯,所以在」main線程「中調用」保姆(線程thread)「的join()方法,讓」保姆(線程thread)「 把飯作完了再通知本身去吃飯。
public class JoinDemo {

    public static void main(String[] args) {
        System.out.println("肚子餓了,想吃飯飯");

        Thread thread = new Thread(() -> {
            System.out.println("開始作飯飯");
            try {
                // 讓線程休眠10秒鐘,模擬作飯的過程
                Thread.sleep(10000l);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("飯飯作好了");
        });
        thread.start();

        try {
            // 等待飯作好
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 只有飯作好了,才能開始吃飯
        System.out.println("開始吃飯飯");

    }
}
複製代碼

原理

  • join()方法的使用很簡單,下面來看下它的實現原理。
  • join()方法的做用,其本質實際上就是線程之間的通訊。CyclicBarrier和CountDownLatch這兩個類的做用的本質也是線程之間的通訊,在源碼分析中,咱們能夠發現,這兩個類的底層最終是經過AQS來實現的,而AQS中是經過LockSupport類的park()unpark()方法來實現線程通訊的。而Thread類的join()則是經過Object類的waitnotify()、notifyAll()方法來實現的。
  • 當調用thread.join()時,爲何能讓線程阻塞呢?它是如何實現的呢?答案就在join()方法的源碼當中。join()方法有3個重載方法,其餘兩個方法支持超時等待,當超過指定時間後,若是子線程尚未執行完成,那麼主線程就會直接醒來。當調用join()方法中,會直接調用join(0)方法,參數傳0表示不限時長地等待。join(long millis)方法的源碼以下。
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");
    }

    // 當millis爲0時,表示不限時長地等待
    if (millis == 0) {
        // 經過while()死循環,只要線程還活着,那麼就等待
        while (isAlive()) {
            wait(0);
        }
    } else {
        // 當millis不爲0時,就須要進行超時時間的計算,而後讓線程等待指定的時間
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}
複製代碼
  • 從上面的源碼中,能夠發現,join(long millis)方法的簽名中,加了synchronized關鍵字來保證線程安全,join(long millis)最終調用的是Object對象的wait()方法,讓主線程等待。這裏須要注意的是,synchronized關鍵字實現的隱式鎖,鎖的是this,即thread這個對象(由於synchronized修飾的join(long millis)方法是一個成員方法,所以鎖的是實例對象),在Demo中,咱們是在main線程中,調用了thread這個對象的join()方法,因此這裏調用wait()方法時,調用的是thread這個對象的wait()方法,因此是main線程進入到等待狀態中。(調用的是thread這個對象的wait()方法,這一點很重要,由於後面喚醒main線程時,須要用thread這個對象的notify()或者notifyAll()方法)。
  • 那麼問題來了,既然調用了wait()方法,那麼notify()或者notifyAll()方法是在哪兒被調用的?然而咱們找遍了join()方法的源碼以及咱們本身寫的Demo代碼,都沒有看到notify()或者notifyAll()方法的身影。咱們都知道,wait()和notify()或者notifyAll()確定是成對出現的,單獨使用它們毫無心義,那麼在join()方法的使用場景下,notify()或者notifyAll()方法是在哪兒被調用的呢?答案就是jvm
  • Java裏面的Thread類在JVM上對應的文件是thread.cpp。thread.cpp文件的路徑是jdk-jdk8-b120/hotspot/src/share/vm/runtime/thread.cpp(筆者本地下載的是openJDK8的源代碼)。在thread.cpp文件中定義了不少和線程相關的方法,Thread.java類中的native方法就是在thread.cpp中實現的。
  • 根據join()方法的描述,它的做用是先讓thread業務邏輯執行完成,而後才讓main線程開始執行。因此咱們能夠猜想notify()或者notifyAll()方法應該是在線程執行完run()方法後,JVM對線程作一些收尾工做時調用的。在JVM中,當每一個線程執行完成時,會調用到thread.cpp文件中JavaThread::exit(bool destroy_vm, ExitType exit_type)方法(該方法的代碼在1730行附近)。exit()方法的源碼很長,差很少200行,刪除了無用的代碼,只保留了和今天主題有關的內容。源碼以下。
void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
	assert(this == JavaThread::current(),  "thread consistency check");
	// ...省略了不少代碼

	// Notify waiters on thread object. This has to be done after exit() is called
	// on the thread (if the thread is the last thread in a daemon ThreadGroup the
	// group should have the destroyed bit set before waiters are notified).
    
    // 關鍵代碼就在這一行,從方法名就能夠推斷出,它是線程在退出時,用來確保join()方法的相關邏輯的。而這裏的this就是指的當前線程。
    // 從上面的英文註釋也能看出,它是用來處理join()相關的邏輯的
	ensure_join(this);

	// ...省略了不少代碼

}
複製代碼
  • 能夠看到,主要邏輯在ensure_join()方法中,接着找到ensure_join()方法的源碼,源碼以下。(ensure_join()方法的源碼也在thread.cpp文件當中。有興趣的朋友可使用JetBrains公司提供的Clion這款智能工具來查看JVM的源代碼,和IDEA產很少,按住option(或者Alt鍵)+鼠標左鍵,也能跟蹤到源代碼中)。
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
  java_lang_Thread::set_thread(threadObj(), NULL);


  // 核心代碼在下面這一行
  // 是否是很驚喜?果真見到了和notify相關的單詞,不用懷疑,notify_all()方法確定就是用來喚醒。這裏的thread對象就是咱們demo中的子線程thread這個實例對象
  lock.notify_all(thread);


  // Ignore pending exception (ThreadDeath), since we are exiting anyway
  thread->clear_pending_exception();
}
複製代碼

總結

  • 本文主要介紹了join()方法的做用是用來控制線程的執行順序的,並結合Demo演示了其用法,而後結合源代碼分析了join()方法的實現原理。join(long millis)方法由於使用了synchronized關鍵字修飾,因此是一個同步方法,它鎖的對象是this,也就是實例對象自己。在join(long millis)方法中調用了實例對象的wait()方法,而notifyAll()方法是在jvm中調用的。在實際開發過程當中,join()使用的比較少,咱們一般會使用JUC包下提供的工具類CountDownLatch或者CyclicBarrier,由於後二者的功能更增強大。
  • 在join(long millis)方法的源碼中,隱藏了一個經典的編程範式。以下。
// 這種寫法是等待/通知的經典範式。
while(條件判斷){
	wait();
}
複製代碼

推薦

微信公衆號
相關文章
相關標籤/搜索