Thread.join()的實現原理

掃描下方二維碼或者微信搜索公衆號菜鳥飛呀飛,即可關注微信公衆號,閱讀更多Spring源碼分析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();
}

推薦

微信公衆號

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